diff --git a/backend/WorkClub.Api/Services/ClubService.cs b/backend/WorkClub.Api/Services/ClubService.cs index 4578479..aaa777c 100644 --- a/backend/WorkClub.Api/Services/ClubService.cs +++ b/backend/WorkClub.Api/Services/ClubService.cs @@ -112,7 +112,8 @@ public class ClubService clubId.Value, clubName, sportTypeEnum, - memberCount + memberCount, + Guid.Parse(tenantId) )); } } diff --git a/backend/WorkClub.Application/Clubs/DTOs/ClubListDto.cs b/backend/WorkClub.Application/Clubs/DTOs/ClubListDto.cs index c6b02bc..cca0e9e 100644 --- a/backend/WorkClub.Application/Clubs/DTOs/ClubListDto.cs +++ b/backend/WorkClub.Application/Clubs/DTOs/ClubListDto.cs @@ -4,4 +4,5 @@ public record ClubListDto( Guid Id, string Name, string SportType, - int MemberCount); + int MemberCount, + Guid TenantId); diff --git a/frontend/next.config.ts b/frontend/next.config.ts index 225e495..0956e5e 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -2,6 +2,15 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: 'standalone', + async rewrites() { + const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5001'; + return [ + { + source: '/api/:path((?!auth).*)', + destination: `${apiUrl}/api/:path*`, + }, + ]; + }, }; export default nextConfig; diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx index d7509aa..327b66f 100644 --- a/frontend/src/app/login/page.tsx +++ b/frontend/src/app/login/page.tsx @@ -1,12 +1,24 @@ 'use client'; -import { signIn } from 'next-auth/react'; +import { useEffect } from 'react'; +import { signIn, useSession } from 'next-auth/react'; +import { useRouter } from 'next/navigation'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; export default function LoginPage() { + const { status } = useSession(); + const router = useRouter(); + + // Redirect to dashboard if already authenticated + useEffect(() => { + if (status === 'authenticated') { + router.push('/dashboard'); + } + }, [status, router]); + const handleSignIn = () => { - signIn('keycloak'); + signIn('keycloak', { callbackUrl: '/dashboard' }); }; return ( diff --git a/frontend/src/components/auth-guard.tsx b/frontend/src/components/auth-guard.tsx index c18c82e..ca88c2b 100644 --- a/frontend/src/components/auth-guard.tsx +++ b/frontend/src/components/auth-guard.tsx @@ -7,7 +7,7 @@ import { useTenant } from '../contexts/tenant-context'; export function AuthGuard({ children }: { children: ReactNode }) { const { data: session, status } = useSession(); - const { activeClubId, clubs, setActiveClub } = useTenant(); + const { activeClubId, clubs, setActiveClub, clubsLoading } = useTenant(); const router = useRouter(); useEffect(() => { @@ -38,6 +38,14 @@ export function AuthGuard({ children }: { children: ReactNode }) { return null; } + if (status === 'authenticated' && clubsLoading) { + return ( +
+

Loading...

+
+ ); + } + if (clubs.length === 0 && status === 'authenticated') { return (
diff --git a/frontend/src/contexts/tenant-context.tsx b/frontend/src/contexts/tenant-context.tsx index 0a67012..94d46ae 100644 --- a/frontend/src/contexts/tenant-context.tsx +++ b/frontend/src/contexts/tenant-context.tsx @@ -8,6 +8,7 @@ type Club = { id: string; name: string; sportType: string; + tenantId: string; }; type TenantContextType = { @@ -15,6 +16,8 @@ type TenantContextType = { setActiveClub: (clubId: string) => void; userRole: string | null; clubs: Club[]; + clubsLoading: boolean; + clubsError: Error | null; }; const TenantContext = createContext(undefined); @@ -24,14 +27,20 @@ export function TenantProvider({ children }: { children: ReactNode }) { const [activeClubId, setActiveClubId] = useState(null); const queryClient = useQueryClient(); - const { data: clubs = [] } = useQuery({ - queryKey: ['my-clubs'], + const { data: clubs = [], isLoading: clubsLoading, error: clubsError } = useQuery({ + queryKey: ['my-clubs', session?.accessToken], queryFn: async () => { - const res = await fetch('/api/clubs/me'); - if (!res.ok) return []; + const res = await fetch('/api/clubs/me', { + headers: { + Authorization: `Bearer ${session?.accessToken}`, + }, + }); + if (!res.ok) throw new Error(`Failed to fetch clubs: ${res.statusText}`); return res.json(); }, - enabled: status === 'authenticated', + enabled: status === 'authenticated' && !!session?.accessToken, + retry: 3, + retryDelay: (attemptIndex) => Math.min(1000 * Math.pow(2, attemptIndex), 10000), }); useEffect(() => { @@ -47,21 +56,27 @@ export function TenantProvider({ children }: { children: ReactNode }) { useEffect(() => { if (activeClubId) { - document.cookie = `X-Tenant-Id=${activeClubId}; path=/; max-age=86400`; + const selectedClub = clubs.find(c => c.id === activeClubId); + if (selectedClub) { + document.cookie = `X-Tenant-Id=${selectedClub.tenantId}; path=/; max-age=86400`; + } } - }, [activeClubId]); + }, [activeClubId, clubs]); const handleSetActiveClub = (clubId: string) => { setActiveClubId(clubId); localStorage.setItem('activeClubId', clubId); - document.cookie = `X-Tenant-Id=${clubId}; path=/; max-age=86400`; + const selectedClub = clubs.find(c => c.id === clubId); + if (selectedClub) { + document.cookie = `X-Tenant-Id=${selectedClub.tenantId}; path=/; max-age=86400`; + } queryClient.invalidateQueries(); }; const userRole = activeClubId && session?.user?.clubs ? session.user.clubs[activeClubId] || null : null; return ( - + {children} ); diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 61b4dc6..4002d74 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,22 +1,31 @@ import { getSession } from 'next-auth/react'; -const ACTIVE_CLUB_KEY = 'activeClubId'; +function getTenantIdFromCookie(): string | null { + if (typeof document === 'undefined') { + return null; + } + + try { + const cookies = document.cookie.split(';'); + for (const cookie of cookies) { + const [name, value] = cookie.trim().split('='); + if (name === 'X-Tenant-Id') { + return decodeURIComponent(value); + } + } + } catch { + // Cookie parsing may fail in some environments + } + + return null; +} export async function apiClient( url: string, options: RequestInit = {} ): Promise { const session = await getSession(); - let activeClubId: string | null = null; - - try { - activeClubId = typeof localStorage !== 'undefined' - ? localStorage.getItem(ACTIVE_CLUB_KEY) - : null; - } catch { - // localStorage may not be available in some environments - activeClubId = null; - } + const tenantId = getTenantIdFromCookie(); const headers: Record = { 'Content-Type': 'application/json', @@ -27,8 +36,8 @@ export async function apiClient( headers['Authorization'] = `Bearer ${session.accessToken}`; } - if (activeClubId) { - headers['X-Tenant-Id'] = activeClubId; + if (tenantId) { + headers['X-Tenant-Id'] = tenantId; } return fetch(url, {