import { useEffect, useState, type ReactNode } from 'react'; import { useDispatch } from 'react-redux'; import { authStorage } from '../utils/auth-storage'; import { hydrateAuth, logout, setUser } from '../store/auth.slice'; import type { AuthUser } from '../types/auth.types'; const BASE_API_URL = (import.meta as { env?: Record }).env?.['VITE_BASE_API_URL'] ?? 'http://localhost:3000/api'; /** * Restores the signed-in session before the router renders. * * Redux starts empty on every page load while the token lives in * localStorage, so without this a refresh leaves the app half–signed-in: the * route guards see a token and let you through, but pages that read * `auth.user` think you are a stranger. * * A token the server no longer accepts is cleared here rather than left to * strand the user on a page they cannot act on or sign out of. */ export function AuthBootstrap({ children }: { children: ReactNode }) { const dispatch = useDispatch(); const [ready, setReady] = useState(false); useEffect(() => { let cancelled = false; async function restore() { dispatch(hydrateAuth()); const token = authStorage.getToken(); const cachedUser = authStorage.getUser(); if (!token) { if (!cancelled) setReady(true); return; } try { const response = await fetch(`${BASE_API_URL}/auth/me`, { headers: { Authorization: `Bearer ${token}` }, }); if (response.ok) { const user = (await response.json()) as AuthUser; // Keep the persisted session as the source of truth when it is // available. A successful profile update writes it immediately, // while `/auth/me` can briefly return a stale read and otherwise // undo that update on every page refresh. We still make this call // to validate the token and clear invalid sessions below. if (!cancelled && !cachedUser) dispatch(setUser(user)); } else if (response.status === 401 || response.status === 403) { // Expired or revoked — drop it so the user gets a login screen // instead of a dead end. if (!cancelled) dispatch(logout()); } } catch { // Offline or the API is down: keep the stored session and let the // individual screens surface their own errors. } finally { if (!cancelled) setReady(true); } } restore(); return () => { cancelled = true; }; }, [dispatch]); // Rendering the router before the session resolves would let the guards // redirect based on a state that is about to change. if (!ready) return null; return <>{children}; }