// AuthProvider — backend-mediated OIDC login. // // Flow: // 1. On mount, GET /api/auth/oidc/status to learn if OIDC is configured. // 2. GET /api/auth/me — if session cookie exists + valid → set user. // 3. If anon + click Login → window.location = /api/auth/oidc/start // 4. Backend 302's to IdP → IdP 302's back to /api/auth/oidc/callback // → backend 302's to /oidc/callback#oidc_ticket=... on this SPA. // 5. reads the fragment, POST /api/auth/oidc/exchange, // backend sets the dialectic_session cookie, redirects to /. // 6. Logout: POST /api/auth/logout (clears cookie) → useAuth refetches. // // The session JWT lives only in an HttpOnly cookie; the SPA never sees // the raw token. /api/auth/me is the canonical "who am I" probe. import { createContext, useCallback, useContext, useEffect, useState, type ReactNode, } from 'react'; interface User { id: string; email: string; name: string; } interface AuthCtx { user: User | null; loading: boolean; oidcEnabled: boolean; login: () => void; logout: () => Promise; refresh: () => Promise; } const Ctx = createContext({ user: null, loading: true, oidcEnabled: false, login: () => {}, logout: async () => {}, refresh: async () => {}, }); const API_BASE = import.meta.env.VITE_DIALECTIC_API_BASE ?? '/api'; async function fetchStatus(): Promise { try { const r = await fetch(`${API_BASE}/auth/oidc/status`); if (!r.ok) return false; const j = await r.json(); return Boolean(j.enabled); } catch { return false; } } async function fetchMe(): Promise { try { const r = await fetch(`${API_BASE}/auth/me`, { credentials: 'same-origin' }); if (r.status === 401) return null; if (!r.ok) return null; return (await r.json()) as User; } catch { return null; } } export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [oidcEnabled, setOidcEnabled] = useState(false); const refresh = useCallback(async () => { setLoading(true); const [enabled, me] = await Promise.all([fetchStatus(), fetchMe()]); setOidcEnabled(enabled); setUser(me); setLoading(false); }, []); useEffect(() => { refresh(); }, [refresh]); const login = useCallback(() => { // Hard navigation — backend builds the IdP URL + redirects. window.location.href = `${API_BASE}/auth/oidc/start`; }, []); const logout = useCallback(async () => { await fetch(`${API_BASE}/auth/logout`, { method: 'POST', credentials: 'same-origin', }); setUser(null); }, []); return ( {children} ); } export function useAuth(): AuthCtx { return useContext(Ctx); }