import { createContext, useContext, useState, useEffect, useCallback, type ReactNode, } from "react"; import { AUTH_TOKEN_KEY, authLogin, authGetMe, authRegister, impersonateUser, stopImpersonation as apiStopImpersonation, ApiError, type UserResponse, type RegisterRequest, } from "../api"; const ADMIN_TOKEN_KEY = "chrysopedia_admin_token"; interface AuthContextValue { user: UserResponse | null; token: string | null; isAuthenticated: boolean; isImpersonating: boolean; loading: boolean; login: (email: string, password: string) => Promise; register: (data: RegisterRequest) => Promise; logout: () => void; startImpersonation: (userId: string) => Promise; exitImpersonation: () => Promise; } const AuthContext = createContext(null); export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null); const [token, setToken] = useState(() => { try { return localStorage.getItem(AUTH_TOKEN_KEY); } catch { return null; } }); const [loading, setLoading] = useState(!!token); // Rehydrate session from stored token on mount useEffect(() => { if (!token) return; let cancelled = false; authGetMe(token) .then((u) => { if (!cancelled) setUser(u); }) .catch(() => { // Token expired or invalid — clear it if (!cancelled) { localStorage.removeItem(AUTH_TOKEN_KEY); setToken(null); setUser(null); } }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, []); // eslint-disable-line react-hooks/exhaustive-deps const login = useCallback(async (email: string, password: string) => { const resp = await authLogin(email, password); localStorage.setItem(AUTH_TOKEN_KEY, resp.access_token); setToken(resp.access_token); const me = await authGetMe(resp.access_token); setUser(me); }, []); const register = useCallback(async (data: RegisterRequest) => { return authRegister(data); }, []); const logout = useCallback(() => { localStorage.removeItem(AUTH_TOKEN_KEY); sessionStorage.removeItem(ADMIN_TOKEN_KEY); setToken(null); setUser(null); }, []); const startImpersonation = useCallback(async (userId: string) => { if (!token) return; // Save admin token so we can restore it later sessionStorage.setItem(ADMIN_TOKEN_KEY, token); const resp = await impersonateUser(token, userId); localStorage.setItem(AUTH_TOKEN_KEY, resp.access_token); setToken(resp.access_token); const me = await authGetMe(resp.access_token); setUser(me); }, [token]); const exitImpersonation = useCallback(async () => { // Try to call stop endpoint for audit log if (token) { try { await apiStopImpersonation(token); } catch { // Best effort — still restore admin session } } // Restore admin token const adminToken = sessionStorage.getItem(ADMIN_TOKEN_KEY); sessionStorage.removeItem(ADMIN_TOKEN_KEY); if (adminToken) { localStorage.setItem(AUTH_TOKEN_KEY, adminToken); setToken(adminToken); const me = await authGetMe(adminToken); setUser(me); } else { // Fallback: just logout logout(); } }, [token, logout]); return ( {children} ); } export function useAuth(): AuthContextValue { const ctx = useContext(AuthContext); if (!ctx) { throw new Error("useAuth must be used within an AuthProvider"); } return ctx; } export { ApiError };