"use client"; import { createContext, useCallback, useContext, useEffect, useState, ReactNode, } from "react"; type AuthUser = { uuid: string; email: string; username?: string | null; passwordSetAt?: string | null; displayName?: string | null; role: string; } | null; export type RegisterParams = { email: string; password: string; displayName?: string; acceptedTos: boolean; tosVersion: string; }; type AuthContextValue = { user: AuthUser; loading: boolean; login: (params: { email: string; password: string }) => Promise; register: (params: RegisterParams) => Promise; logout: () => void; refreshUser: () => Promise; getAuthHeaders: () => HeadersInit; }; const AuthContext = createContext(undefined); const USER_KEY = "ballistic_auth_user"; export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); /** * Persist user to localStorage (for quick initial render) */ const persistUser = useCallback((nextUser: AuthUser) => { if (typeof window === "undefined") return; if (nextUser) { window.localStorage.setItem(USER_KEY, JSON.stringify(nextUser)); } else { window.localStorage.removeItem(USER_KEY); } }, []); /** * Hydrate from localStorage and refresh from server */ useEffect(() => { if (typeof window === "undefined") return; const storedUser = window.localStorage.getItem(USER_KEY); // Quick hydration from cached user if (storedUser) { try { const parsedUser = JSON.parse(storedUser); setUser(parsedUser); } catch { window.localStorage.removeItem(USER_KEY); } } // Verify session with server fetch("/api/auth/me", { credentials: 'include' }) .then((res) => { if (res.ok) { return res.json(); } throw new Error("Not authenticated"); }) .then((data) => { const nextUser: AuthUser = { uuid: data.uuid, email: data.email, username: data.username || null, displayName: data.displayName || null, role: data.role || "USER", passwordSetAt: data.passwordSetAt || null, }; setUser(nextUser); persistUser(nextUser); }) .catch(() => { setUser(null); persistUser(null); }) .finally(() => { setLoading(false); }); }, [persistUser]); const refreshUser = useCallback(async () => { const res = await fetch("/api/auth/me", { cache: "no-store", credentials: 'include', }); if (!res.ok) { const text = await res.text().catch(() => res.statusText); throw new Error(text || "Failed to refresh user"); } const data = await res.json(); const nextUser: AuthUser = { uuid: data.uuid, email: data.email, username: data.username || null, displayName: data.displayName || null, role: data.role || "USER", passwordSetAt: data.passwordSetAt || null, }; setUser(nextUser); persistUser(nextUser); }, [persistUser]); const login = useCallback( async ({ email, password }: { email: string; password: string }) => { setLoading(true); try { const res = await fetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: 'include', body: JSON.stringify({ email, password }), }); if (!res.ok) { const errorData = await res.json().catch(() => ({})); throw new Error(errorData.error || "Login failed"); } const data = await res.json(); const nextUser: AuthUser = data.user; setUser(nextUser); persistUser(nextUser); } finally { setLoading(false); } }, [persistUser] ); const register = useCallback( async ({ email, password, displayName, acceptedTos, tosVersion, }: RegisterParams) => { setLoading(true); try { const res = await fetch("/api/auth/register", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: 'include', body: JSON.stringify({ email, password, displayName, acceptedTos, tosVersion, }), }); if (!res.ok) { const errorData = await res.json().catch(() => ({})); throw new Error(errorData.error || "Registration failed"); } const data = await res.json(); const nextUser: AuthUser = data.user; setUser(nextUser); persistUser(nextUser); } finally { setLoading(false); } }, [persistUser] ); const logout = useCallback(() => { setUser(null); persistUser(null); // Clear server session cookies fetch("/api/auth/logout", { method: "POST", credentials: 'include', }).catch(() => {}); }, [persistUser]); const getAuthHeaders = useCallback((): HeadersInit => { if (typeof window === "undefined") return {}; const token = localStorage.getItem("token") || localStorage.getItem("jwt") || localStorage.getItem("accessToken"); return token ? { Authorization: `Bearer ${token}` } : {}; }, []); const value: AuthContextValue = { user, loading, login, register, logout, refreshUser, getAuthHeaders, }; return {children}; } export function useAuth(): AuthContextValue { const ctx = useContext(AuthContext); if (!ctx) { throw new Error("useAuth must be used within an AuthProvider"); } return ctx; }