import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from "react"; import { api, onUnauthorized } from "../api/client"; import type { CurrentUser, Role, SystemStatus } from "../api/types"; interface AuthState { user: CurrentUser | null; loading: boolean; demoMode: boolean; loginAs: (role: Role) => Promise; loginWithPassword: (email: string, password: string) => Promise; logout: () => Promise; } const AuthContext = createContext(undefined); const STORAGE_KEY = "mobilityops.demo-user"; function readCachedUser(): CurrentUser | null { const stored = sessionStorage.getItem(STORAGE_KEY); return stored ? (JSON.parse(stored) as CurrentUser) : null; } function cacheUser(user: CurrentUser | null) { if (user) { sessionStorage.setItem(STORAGE_KEY, JSON.stringify(user)); } else { sessionStorage.removeItem(STORAGE_KEY); } } export function AuthProvider({ children }: { children: ReactNode }) { // The cached value only avoids a login-screen flash while the server round trip below is // in flight; it is never trusted on its own. `loading` stays true until the server has // confirmed (or rejected) the session cookie. const [user, setUser] = useState(() => readCachedUser()); const [loading, setLoading] = useState(true); const [demoMode, setDemoMode] = useState(true); useEffect(() => { let cancelled = false; api.get("/api/v1/system/status") .then((system) => setDemoMode(system.demo_mode)) .catch(() => setDemoMode(true)) .finally(() => api .get("/api/v1/auth/session") .then((confirmed) => { if (cancelled) return; setUser(confirmed); cacheUser(confirmed); }) .catch(() => { if (cancelled) return; setUser(null); cacheUser(null); }) .finally(() => { if (!cancelled) setLoading(false); })); return () => { cancelled = true; }; }, []); useEffect( () => onUnauthorized(() => { setUser(null); cacheUser(null); }), [], ); const loginAs = useCallback(async (role: Role) => { setLoading(true); try { const loggedIn = await api.post("/api/v1/demo/login", { role }); setUser(loggedIn); cacheUser(loggedIn); } finally { setLoading(false); } }, []); const loginWithPassword = useCallback(async (email: string, password: string) => { setLoading(true); try { const loggedIn = await api.post("/api/v1/auth/login", { email, password }); setUser(loggedIn); cacheUser(loggedIn); } finally { setLoading(false); } }, []); const logout = useCallback(async () => { setUser(null); cacheUser(null); try { await api.post("/api/v1/auth/logout"); } catch { // Best effort: the cookie is cleared server-side when it works, and the client has // already dropped its own state either way. } }, []); return ( {children} ); } export function useAuth(): AuthState { const ctx = useContext(AuthContext); if (!ctx) throw new Error("useAuth must be used within AuthProvider"); return ctx; }