hmc-rag / frontend /src /hooks /useAuth.ts
webmuppet
Refactor: replace Streamlit with React+Vite SPA + FastAPI server
b5c0df4
Raw
History Blame Contribute Delete
3.27 kB
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 };
}