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 (
+
+ );
+ }
+
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, {