/* eslint-disable react-refresh/only-export-components -- contexte + hook + provider colocalisés */ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode, } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { EDITION_ID, api, setAuthToken, setOnUnauthorized } from '@/api'; import type { AccountRead } from '@/api/schema'; const STORAGE_KEY = 'benevoles.token'; function readStoredToken(): string | null { try { return window.localStorage.getItem(STORAGE_KEY); } catch { return null; } } function writeStoredToken(token: string | null): void { try { if (token) window.localStorage.setItem(STORAGE_KEY, token); else window.localStorage.removeItem(STORAGE_KEY); } catch { /* stockage indisponible : la session ne survivra pas au rechargement */ } } type AuthStatus = 'loading' | 'anon' | 'authed'; interface AuthContextValue { status: AuthStatus; account: AccountRead | null; loginWithPassword: (email: string, password: string) => Promise; requestMagicLink: (email: string) => Promise; verifyMagicLink: (token: string) => Promise; logout: () => void; } // Exporté pour les tests / le rendu headless ; l'app passe par . export const AuthContext = createContext(null); export type { AuthContextValue }; export function AuthProvider({ children }: { children: ReactNode }) { const queryClient = useQueryClient(); const [status, setStatus] = useState('loading'); const [account, setAccount] = useState(null); const applySession = useCallback( (token: string, nextAccount: AccountRead) => { writeStoredToken(token); setAuthToken(token); setAccount(nextAccount); setStatus('authed'); queryClient.clear(); }, [queryClient], ); const logout = useCallback(() => { writeStoredToken(null); setAuthToken(null); setAccount(null); setStatus('anon'); queryClient.clear(); }, [queryClient]); // 401 sur une requête protégée ⇒ déconnexion. const logoutRef = useRef(logout); logoutRef.current = logout; useEffect(() => { setOnUnauthorized(() => logoutRef.current()); return () => setOnUnauthorized(null); }, []); // Réhydratation au chargement. useEffect(() => { const token = readStoredToken(); if (!token) { setStatus('anon'); return; } setAuthToken(token); let cancelled = false; api.auth .me() .then((me) => { if (cancelled) return; setAccount(me); setStatus('authed'); }) .catch(() => { if (cancelled) return; writeStoredToken(null); setAuthToken(null); setStatus('anon'); }); return () => { cancelled = true; }; }, []); const value = useMemo( () => ({ status, account, loginWithPassword: async (email, password) => { const res = await api.auth.login(EDITION_ID, { email, password }); applySession(res.access_token, res.account); return res.account; }, requestMagicLink: (email) => api.auth.requestMagicLink(EDITION_ID, { email }), verifyMagicLink: async (token) => { const res = await api.auth.verifyMagicLink(EDITION_ID, { token }); applySession(res.access_token, res.account); return res.account; }, logout, }), [status, account, applySession, logout], ); return {children}; } export function useAuth(): AuthContextValue { const ctx = useContext(AuthContext); if (!ctx) throw new Error('useAuth doit être utilisé dans '); return ctx; }