import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import * as authApi from "../api/auth"; import { setTokenProvider, setUnauthorizedHandler } from "../api/client"; import type { Permission, Role, User } from "../api/types"; import { AuthContext, type AuthState } from "./AuthContext"; const STORAGE_KEY = "gc.token"; /** * Token storage policy: the source of truth is an in-memory ref (survives * re-renders, never serialized into React state to avoid accidental logging). * It is mirrored into sessionStorage — NOT localStorage — so it clears when the * tab closes and never leaks across browser sessions. */ export function AuthProvider({ children }: { children: React.ReactNode }) { const tokenRef = useRef(sessionStorage.getItem(STORAGE_KEY)); const [user, setUser] = useState(null); const [initializing, setInitializing] = useState(true); const setToken = useCallback((token: string | null) => { tokenRef.current = token; if (token) sessionStorage.setItem(STORAGE_KEY, token); else sessionStorage.removeItem(STORAGE_KEY); }, []); // Wire the API client to read our token and to notify us on 401. useEffect(() => { setTokenProvider(() => tokenRef.current); }, []); const clearSession = useCallback(() => { setToken(null); setUser(null); }, [setToken]); useEffect(() => { setUnauthorizedHandler(clearSession); return () => setUnauthorizedHandler(null); }, [clearSession]); // Restore session on first load if a token is present. useEffect(() => { let cancelled = false; async function restore() { if (!tokenRef.current) { setInitializing(false); return; } try { const me = await authApi.getMe(); if (!cancelled) setUser(me); } catch { // Invalid/expired token — clear it. The 401 handler also fires, but // guard here for non-401 failures too. if (!cancelled) clearSession(); } finally { if (!cancelled) setInitializing(false); } } void restore(); return () => { cancelled = true; }; }, [clearSession]); const login = useCallback( async (username: string, password: string) => { const res = await authApi.login({ username, password }); setToken(res.token); setUser(res.user); }, [setToken], ); const logout = useCallback(async () => { try { // Best-effort server-side revocation; clear locally regardless. await authApi.logout(); } catch { // ignore — token may already be invalid } finally { clearSession(); } }, [clearSession]); const value = useMemo(() => { const role = user?.role; return { user, initializing, login, logout, isAdmin: role === "admin", hasRole: (r: Role) => role === r, hasPermission: (p: Permission) => user?.permissions.includes(p) ?? false, }; }, [user, initializing, login, logout]); return {children}; }