import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { fetchMe, loginWithEmailPassword, MeResponse, switchTenantRequest, Tenant } from '@/src/lib/api'; import { clearStoredToken, getStoredToken, setStoredToken, tokenStorageInfo } from '@/src/lib/token-storage'; export type AuthUser = MeResponse['user']; type AuthContextValue = { isBootstrapping: boolean; token: string | null; user: AuthUser | null; tenants: Tenant[]; activeTenantId: number | null; activeTenant: Tenant | null; profile: Record | null; permissions: string[]; requiresTenantSelection: boolean; login: (email: string, password: string) => Promise; logout: () => Promise; refreshUser: () => Promise; switchTenant: (tenantId: number) => Promise; }; const AuthContext = createContext(undefined); function normalizeTenantId(value: number | string | null | undefined): number | null { if (value === null || value === undefined || value === '') { return null; } const parsed = Number(value); return Number.isNaN(parsed) ? null : parsed; } export function AuthProvider({ children }: { children: React.ReactNode }) { const [isBootstrapping, setIsBootstrapping] = useState(true); const [token, setToken] = useState(null); const [user, setUser] = useState(null); const [tenants, setTenants] = useState([]); const [activeTenantId, setActiveTenantId] = useState(null); const [profile, setProfile] = useState | null>(null); const [permissions, setPermissions] = useState([]); const resetSession = useCallback(() => { setUser(null); setTenants([]); setActiveTenantId(null); setProfile(null); setPermissions([]); }, []); const hydrateSession = useCallback( async (tokenToUse: string) => { const me = await fetchMe(tokenToUse); setUser(me.user); setTenants(me.tenants || []); setActiveTenantId(normalizeTenantId(me.activeTenant)); setProfile(me.profile || null); setPermissions(me.permissions || []); }, [] ); const logout = useCallback(async () => { await clearStoredToken(); setToken(null); resetSession(); }, [resetSession]); const refreshUser = useCallback(async () => { if (!token) { resetSession(); return; } try { await hydrateSession(token); } catch { await logout(); } }, [hydrateSession, logout, resetSession, token]); useEffect(() => { async function bootstrap() { const storedToken = await getStoredToken(); if (!storedToken) { setIsBootstrapping(false); return; } try { await hydrateSession(storedToken); setToken(storedToken); } catch { await clearStoredToken(); setToken(null); resetSession(); } finally { setIsBootstrapping(false); } } void bootstrap(); }, [hydrateSession, resetSession]); const login = useCallback( async (email: string, password: string) => { const nextToken = await loginWithEmailPassword(email, password); await setStoredToken(nextToken); await hydrateSession(nextToken); setToken(nextToken); }, [hydrateSession] ); const switchTenant = useCallback( async (tenantId: number) => { if (!token) { throw new Error('No active session found.'); } const nextToken = await switchTenantRequest(tenantId, token); await setStoredToken(nextToken); await hydrateSession(nextToken); setToken(nextToken); }, [token, hydrateSession] ); const activeTenant = useMemo( () => tenants.find((tenant) => Number(tenant.id) === activeTenantId) || null, [activeTenantId, tenants] ); const requiresTenantSelection = Boolean(token) && tenants.length > 0 && !activeTenantId; const value = useMemo( () => ({ isBootstrapping, token, user, tenants, activeTenantId, activeTenant, profile, permissions, requiresTenantSelection, login, logout, refreshUser, switchTenant, }), [ activeTenant, activeTenantId, isBootstrapping, login, logout, permissions, profile, refreshUser, requiresTenantSelection, switchTenant, tenants, token, user, ] ); return {children}; } export function useAuth(): AuthContextValue { const context = useContext(AuthContext); if (!context) { throw new Error('useAuth must be used inside AuthProvider'); } return context; } export function useTokenStorageInfo() { return tokenStorageInfo; }