import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { fetchMe, loginWithEmailPassword, MeResponse, switchTenantRequest, Tenant } from '@/src/lib/api'; import { hydrateApiBaseUrl } from '@/src/lib/server-config'; import { clearStoredToken, getStoredToken, setStoredToken, tokenStorageInfo } from '@/src/lib/token-storage'; export type AuthUser = MeResponse['user']; type AuthContextValue = { isBootstrapping: boolean; bootstrapError: string | null; 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; retryBootstrap: () => Promise; resetLocalSession: () => Promise; }; const AuthContext = createContext(undefined); const BOOTSTRAP_TIMEOUT_MS = 20000; 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; } function withTimeout(promise: Promise, timeoutMs: number, message: string): Promise { return new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error(message)), timeoutMs); promise .then(resolve) .catch(reject) .finally(() => clearTimeout(timeout)); }); } export function AuthProvider({ children }: { children: React.ReactNode }) { const [isBootstrapping, setIsBootstrapping] = useState(true); const [bootstrapError, setBootstrapError] = useState(null); 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 bootstrapRunRef = useRef(0); 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); setBootstrapError(null); resetSession(); }, [resetSession]); const refreshUser = useCallback(async () => { if (!token) { resetSession(); return; } try { await hydrateSession(token); } catch { await logout(); } }, [hydrateSession, logout, resetSession, token]); const resetLocalSession = useCallback(async () => { setToken(null); setBootstrapError(null); setIsBootstrapping(false); resetSession(); void clearStoredToken().catch(() => undefined); }, [resetSession]); const bootstrap = useCallback(async () => { const runId = bootstrapRunRef.current + 1; bootstrapRunRef.current = runId; setIsBootstrapping(true); setBootstrapError(null); try { await withTimeout( (async () => { await hydrateApiBaseUrl(); const storedToken = await getStoredToken(); if (!storedToken) { if (bootstrapRunRef.current !== runId) return; setToken(null); resetSession(); return; } await hydrateSession(storedToken); if (bootstrapRunRef.current !== runId) return; setToken(storedToken); })(), BOOTSTRAP_TIMEOUT_MS, 'Die mobile Session konnte nicht rechtzeitig initialisiert werden.' ); } catch (err) { if (bootstrapRunRef.current === runId) { setToken(null); resetSession(); setBootstrapError(err instanceof Error ? err.message : 'Die mobile Session konnte nicht initialisiert werden.'); void clearStoredToken().catch(() => undefined); } } finally { if (bootstrapRunRef.current === runId) { setIsBootstrapping(false); } } }, [hydrateSession, resetSession]); useEffect(() => { void bootstrap(); }, [bootstrap]); 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, bootstrapError, token, user, tenants, activeTenantId, activeTenant, profile, permissions, requiresTenantSelection, login, logout, refreshUser, switchTenant, retryBootstrap: bootstrap, resetLocalSession, }), [ activeTenant, activeTenantId, bootstrap, bootstrapError, isBootstrapping, login, logout, permissions, profile, refreshUser, requiresTenantSelection, resetLocalSession, 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; }