"use client"; import { useState, useEffect, useCallback } from "react"; export interface User { id: string; email: string; displayName?: string; emailVerified: boolean; isAdmin?: boolean; createdAt?: string; } const TOKEN_KEY = "medos_auth_token"; export function useAuth() { const [user, setUser] = useState(null); const [token, setTokenState] = useState(null); const [loading, setLoading] = useState(true); const persistToken = useCallback((t: string | null) => { setTokenState(t); if (t) { localStorage.setItem(TOKEN_KEY, t); const secure = window.location.protocol === "https:" ? "; Secure" : ""; document.cookie = `medos_token=${t}; path=/; max-age=${30 * 86400}; SameSite=Lax${secure}`; } else { localStorage.removeItem(TOKEN_KEY); document.cookie = "medos_token=; path=/; max-age=0"; } }, []); // Restore session on mount. useEffect(() => { const t = localStorage.getItem(TOKEN_KEY); if (!t) { setLoading(false); return; } fetch("/api/auth/me", { headers: { Authorization: `Bearer ${t}` } }) .then((r) => (r.ok ? r.json() : null)) .then((data) => { if (data?.user) { persistToken(t); setUser(data.user); } else { persistToken(null); } }) .catch(() => {}) .finally(() => setLoading(false)); }, [persistToken]); const register = useCallback( async (email: string, password: string, opts?: { displayName?: string }) => { try { const res = await fetch("/api/auth/register", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password, displayName: opts?.displayName }), }); const data = await res.json(); if (!res.ok) return { ok: false as const, error: data.error || "Registration failed" }; persistToken(data.token); setUser(data.user); return { ok: true as const, needsVerification: !data.user.emailVerified }; } catch { return { ok: false as const, error: "Network error" }; } }, [persistToken], ); const login = useCallback( async (email: string, password: string) => { // One retry on cold-start: HF Spaces sleep, and the first request // can timeout while the container wakes. The retry usually // succeeds because the proxy's earlier timeout still nudged the // Space into warming up. const attempt = async () => fetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password }), }); try { let res = await attempt(); let data = await res.json().catch(() => ({})); if (!res.ok && data?.code === "backend_cold_start") { res = await attempt(); data = await res.json().catch(() => ({})); } if (!res.ok) return { ok: false as const, error: data.error || "Login failed" }; persistToken(data.token); setUser(data.user); return { ok: true as const }; } catch { return { ok: false as const, error: "Network error" }; } }, [persistToken], ); const verifyEmail = useCallback( async (code: string) => { try { const t = localStorage.getItem(TOKEN_KEY); const res = await fetch("/api/auth/verify-email", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${t}` }, body: JSON.stringify({ code }), }); const data = await res.json(); if (!res.ok) return { ok: false as const, error: data.error }; setUser((u) => (u ? { ...u, emailVerified: true } : u)); return { ok: true as const }; } catch { return { ok: false as const, error: "Network error" }; } }, [], ); const resendVerification = useCallback(async () => { const t = localStorage.getItem(TOKEN_KEY); await fetch("/api/auth/resend-verification", { method: "POST", headers: { Authorization: `Bearer ${t}` }, }).catch(() => {}); }, []); const forgotPassword = useCallback(async (email: string) => { try { const res = await fetch("/api/auth/forgot-password", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email }), }); const data = await res.json(); return { ok: res.ok, message: data.message || data.error }; } catch { return { ok: false, message: "Network error" }; } }, []); const resetPassword = useCallback( async (email: string, code: string, newPassword: string) => { try { const res = await fetch("/api/auth/reset-password", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, code, newPassword }), }); const data = await res.json(); if (!res.ok) return { ok: false as const, error: data.error }; if (data.token) persistToken(data.token); return { ok: true as const }; } catch { return { ok: false as const, error: "Network error" }; } }, [persistToken], ); const logout = useCallback(async () => { const t = localStorage.getItem(TOKEN_KEY); if (t) fetch("/api/auth/logout", { method: "POST", headers: { Authorization: `Bearer ${t}` } }).catch(() => {}); persistToken(null); setUser(null); }, [persistToken]); /** * Self-service account deletion (GDPR Art. 17). The backend at * DELETE /api/auth/me enforces password re-auth, email match, * admin-self-delete block, and per-IP rate limiting; this hook * only forwards the request and wipes local state on success * (same as logout). On any non-2xx response we surface the * backend's error verbatim so the user sees "Password is * incorrect" / "Email confirmation does not match" / "Too many * deletion attempts" rather than a generic failure. */ const deleteMe = useCallback( async (password: string, confirmEmail: string) => { const t = localStorage.getItem(TOKEN_KEY); if (!t) return { ok: false as const, error: "Not authenticated" }; try { const res = await fetch("/api/auth/me", { method: "DELETE", headers: { "Content-Type": "application/json", Authorization: `Bearer ${t}` }, body: JSON.stringify({ password, confirmEmail }), }); const data = await res.json().catch(() => ({})); if (!res.ok) { return { ok: false as const, error: data.error || "Account deletion failed" }; } persistToken(null); setUser(null); return { ok: true as const, message: data.message }; } catch { return { ok: false as const, error: "Network error" }; } }, [persistToken], ); return { user, token, isAuthenticated: !!user, isGuest: !user, loading, register, login, verifyEmail, resendVerification, forgotPassword, resetPassword, logout, deleteMe, }; }