"use client"; import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from "react"; import { usePathname, useRouter } from "next/navigation"; import { api, isApiError, setCsrf, type Me } from "./api"; interface AuthState { me: Me | null; loading: boolean; refresh: () => Promise; logout: () => Promise; isOwner: boolean; hasScope: (s: string) => boolean; } const Ctx = createContext(null); const PUBLIC_PATHS = ["/login"]; function isPublic(path: string | null): boolean { if (!path) return false; const p = path.replace(/\/+$/, "") || "/"; return PUBLIC_PATHS.includes(p); } export function AuthProvider({ children }: { children: ReactNode }) { const [me, setMe] = useState(null); const [loading, setLoading] = useState(true); const pathname = usePathname(); const router = useRouter(); const refresh = useCallback(async (): Promise => { try { const m = await api.get("/auth/me"); setCsrf(m.csrf_token); setMe(m); return m; } catch (e) { if (isApiError(e) && e.status === 401) { setCsrf(null); setMe(null); return null; } // network/other error: keep previous state, but stop loading setMe(null); return null; } finally { setLoading(false); } }, []); useEffect(() => { void refresh(); }, [refresh]); // Redirect rules: signed out → /login/ ; MFA pending → /login/ (step 2) ; signed in on /login → / useEffect(() => { if (loading) return; const pub = isPublic(pathname); if (!me && !pub) { const next = pathname && pathname !== "/" ? `?next=${encodeURIComponent(pathname)}` : ""; router.replace(`/login/${next}`); } else if (me && !me.mfa_passed && !pub) { router.replace("/login/"); } else if (me && me.mfa_passed && pub) { const next = typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("next") : null; router.replace(next && next.startsWith("/") ? next : "/"); } }, [loading, me, pathname, router]); const logout = useCallback(async () => { try { await api.post("/auth/logout"); } catch { /* ignore */ } setCsrf(null); setMe(null); router.replace("/login/"); }, [router]); const value = useMemo( () => ({ me, loading, refresh, logout, isOwner: me?.role === "owner" || (me?.scopes ?? []).includes("admin"), hasScope: (s: string) => (me?.scopes ?? []).includes(s) || (me?.scopes ?? []).includes("admin"), }), [me, loading, refresh, logout], ); return {children}; } export function useAuth(): AuthState { const c = useContext(Ctx); if (!c) throw new Error("useAuth outside AuthProvider"); return c; }