feat: restrict admin access to club operations and rollout test environment #4

Merged
MasterMito merged 2 commits from epic/admin_rework_second_try into main 2026-03-18 09:16:58 +01:00
5 changed files with 33 additions and 19 deletions
Showing only changes of commit d30895c94a - Show all commits
+1 -1
View File
@@ -12,7 +12,7 @@ export default function ProtectedLayout({
children: React.ReactNode;
}) {
const { data } = useSession();
const isAdmin = (data?.user as any)?.isAdmin;
const isAdmin = data?.user?.isAdmin;
return (
<AuthGuard>
@@ -7,7 +7,6 @@ import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';
import { Badge } from '@/components/ui/badge';
import { useRouter } from 'next/navigation';
import { useSession } from 'next-auth/react';
export default function ShiftDetailPage({ params }: { params: Promise<{ id: string }> }) {
const resolvedParams = use(params);
@@ -15,7 +14,6 @@ export default function ShiftDetailPage({ params }: { params: Promise<{ id: stri
const signUpMutation = useSignUpShift();
const cancelMutation = useCancelSignUp();
const router = useRouter();
const { data: session } = useSession();
if (isLoading) return <div>Loading shift...</div>;
if (!shift) return <div>Shift not found</div>;
+7 -7
View File
@@ -47,14 +47,14 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
async jwt({ token, account }) {
if (account && account.access_token) {
// Add clubs claim from Keycloak access token
token.clubs = (account as any).clubs as Record<string, string> || {}
token.accessToken = account.access_token as string
token.clubs = (account as { clubs?: Record<string, string> }).clubs || {}
token.accessToken = account.access_token
try {
const payload = JSON.parse(Buffer.from((token.accessToken as string).split('.')[1], 'base64').toString());
const roles = payload.realm_access?.roles || [];
const roles = (payload.realm_access?.roles as string[]) || [];
token.isAdmin = roles.includes('admin');
} catch (e) {
} catch {
token.isAdmin = false;
}
}
@@ -63,10 +63,10 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
async session({ session, token }) {
// Expose clubs to client
if (session.user) {
session.user.clubs = (token as any).clubs as Record<string, string> | undefined
session.user.isAdmin = (token as any).isAdmin as boolean | undefined
session.user.clubs = token.clubs as Record<string, string> | undefined
session.user.isAdmin = token.isAdmin as boolean | undefined
}
session.accessToken = (token as any).accessToken as string | undefined
session.accessToken = token.accessToken as string | undefined
return session
}
}
@@ -17,10 +17,11 @@ export function ClubManagement() {
const [isCreating, setIsCreating] = useState(false);
const [newClub, setNewClub] = useState({ name: '', sportType: 'Tennis', description: '' });
const fetchClubs = async () => {
useEffect(() => {
const fetchClubsLocally = async () => {
try {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/admin/clubs`, {
headers: { Authorization: `Bearer ${(session as any)?.accessToken}` },
headers: { Authorization: `Bearer ${session?.accessToken}` },
});
if (res.ok) {
const data = await res.json();
@@ -33,10 +34,25 @@ export function ClubManagement() {
}
};
useEffect(() => {
if (session) fetchClubs();
if (session) fetchClubsLocally();
}, [session]);
const fetchClubs = async () => {
try {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/admin/clubs`, {
headers: { Authorization: `Bearer ${session?.accessToken}` },
});
if (res.ok) {
const data = await res.json();
setClubs(data);
}
} catch (error) {
console.error('Failed to fetch clubs', error);
} finally {
setLoading(false);
}
};
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault();
try {
@@ -44,7 +60,7 @@ export function ClubManagement() {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${(session as any)?.accessToken}`,
Authorization: `Bearer ${session?.accessToken}`,
},
body: JSON.stringify({
name: newClub.name,
@@ -67,7 +83,7 @@ export function ClubManagement() {
try {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/admin/clubs/${id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${(session as any)?.accessToken}` },
headers: { Authorization: `Bearer ${session?.accessToken}` },
});
if (res.ok) {
fetchClubs();
+2 -2
View File
@@ -18,7 +18,7 @@ export function AuthGuard({ children }: { children: ReactNode }) {
useEffect(() => {
if (status === 'authenticated') {
const isAdmin = (data?.user as any)?.isAdmin;
const isAdmin = data?.user?.isAdmin;
// Admin routing
if (isAdmin) {
@@ -59,7 +59,7 @@ export function AuthGuard({ children }: { children: ReactNode }) {
);
}
const isAdmin = (data?.user as any)?.isAdmin;
const isAdmin = data?.user?.isAdmin;
if (clubs.length === 0 && status === 'authenticated' && !isAdmin) {
const handleSwitchAccount = () => {
const keycloakLogoutUrl = `${process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER || 'http://localhost:8080/realms/workclub'}/protocol/openid-connect/logout?redirect_uri=${encodeURIComponent(window.location.origin + '/login')}`;