import { useState, useEffect, useCallback } from "react"; import { login as apiLogin } from "../lib/api"; const TOKEN_KEY = "hmc-token"; const AUTH_CHANGE_EVENT = "hmc-auth-change"; function notifyAuthChange(): void { window.dispatchEvent(new Event(AUTH_CHANGE_EVENT)); } /** * Decode the JWT payload (middle segment) without verifying the signature. * Signature verification is the server's responsibility; this is only used * for client-side expiry checks. */ function decodeJwtPayload(token: string): { exp?: number } | null { try { const parts = token.split("."); if (parts.length !== 3) return null; const payload = atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")); return JSON.parse(payload) as { exp?: number }; } catch { return null; } } /** * Returns true if the token exists and its `exp` claim is in the future. */ function isTokenValid(token: string): boolean { const payload = decodeJwtPayload(token); if (!payload) return false; if (payload.exp === undefined) return true; // no expiry claim — treat as valid return payload.exp * 1000 > Date.now(); } export interface UseAuthReturn { isAuthenticated: boolean; token: string | null; login: (password: string) => Promise; logout: () => void; } /** * Clears the stored JWT and marks the session as unauthenticated. * Exported so that API error handlers (e.g. on 401 responses) can call it * without needing access to the hook's internal state setter. * * Note: This function only clears localStorage. Components should use the * `logout()` function returned by `useAuth` to also update React state. */ export function handle401(): void { localStorage.removeItem(TOKEN_KEY); notifyAuthChange(); } /** * Manages JWT-based authentication state. * * - Reads `hmc-token` from localStorage on mount and validates the `exp` claim. * - Exposes `login(password)` to authenticate and store a new token. * - Exposes `logout()` to clear the token and reset state. * - The exported `handle401()` helper can be called from API error handlers * to clear the token when the server returns HTTP 401. */ export function useAuth(): UseAuthReturn { const [isAuthenticated, setIsAuthenticated] = useState(false); const [token, setToken] = useState(null); // Sync with localStorage on mount / whenever any window signals a change useEffect(() => { function checkToken() { const stored = localStorage.getItem(TOKEN_KEY); if (stored && isTokenValid(stored)) { setToken(stored); setIsAuthenticated(true); } else { localStorage.removeItem(TOKEN_KEY); setToken(null); setIsAuthenticated(false); } } checkToken(); window.addEventListener(AUTH_CHANGE_EVENT, checkToken); return () => window.removeEventListener(AUTH_CHANGE_EVENT, checkToken); }, []); const login = useCallback(async (password: string): Promise => { const { token: newToken } = await apiLogin(password); localStorage.setItem(TOKEN_KEY, newToken); notifyAuthChange(); }, []); const logout = useCallback((): void => { localStorage.removeItem(TOKEN_KEY); notifyAuthChange(); }, []); return { isAuthenticated, token, login, logout }; }