import { useCallback, useContext } from "react"; import { AuthContext } from "../contexts/AuthContext.ts"; import { type AuthResponse } from "../model.ts"; function isTokenExpired(token: string): boolean { try { const payload = JSON.parse(atob(token.split(".")[1])); return typeof payload.exp === "number" && payload.exp * 1000 < Date.now(); } catch { return true; } } export const useAuth = () => { const { authResponse, setAuthResponse } = useContext(AuthContext); const login = useCallback((response: AuthResponse) => { setAuthResponse(response); localStorage.setItem("authResponse", JSON.stringify(response)); }, [setAuthResponse]); const logout = useCallback(() => { setAuthResponse(null); localStorage.removeItem("authResponse"); }, [setAuthResponse]); const authFetch = useCallback(async ( input: RequestInfo, init: RequestInit = {}, ) => { const token = authResponse?.token; if (token && isTokenExpired(token)) { logout(); // Return a synthetic 401 so callers handle it consistently return new Response(null, { status: 401 }); } const isFormData = init.body instanceof FormData; const res = await fetch(input, { ...init, headers: { ...(init.headers ?? {}), // Only attach a token when we actually have one — avoids sending a // `Bearer undefined` header that the server would reject as invalid. ...(token ? { Authorization: `Bearer ${token}` } : {}), // Let the browser set Content-Type for FormData (it includes the boundary) ...(isFormData ? {} : { "Content-Type": "application/json" }), }, }); if (res.status === 401) { logout(); } return res; }, [authResponse?.token, logout]); return { user: authResponse?.user ?? null, token: authResponse?.token ?? null, login, logout, authFetch, }; }; export const useRequiredAuth = () => { const { user, token, login, logout, authFetch } = useAuth(); if (!user) { throw new Error( "Invariant: useRequiredAuth called outside a protected route", ); } return { user, token, login, logout, authFetch }; };