Spaces:
Runtime error
Runtime error
| 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<void>; | |
| 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<string | null>(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<void> => { | |
| 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 }; | |
| } | |