Files
foxhunt/web-dashboard/src/lib/auth.ts
jgrusewski c7d336ec59 safety(web-dashboard): token expiry detection, session-expired banner, mutation error feedback
- Add isTokenExpired() / getTokenExpiry() JWT helpers to auth.ts
- Pre-flight token expiry check in apiFetch (60s margin)
- Graceful 401 handling: set session_expired flag before redirect
- LoginPage shows "session expired" banner when flag is set
- OrdersTable cancel button: add pending state + inline error display

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 09:16:31 +01:00

67 lines
1.9 KiB
TypeScript

const TOKEN_KEY = 'foxhunt_token';
const REFRESH_KEY = 'foxhunt_refresh_token';
/** Store tokens after login */
export function setTokens(accessToken: string, refreshToken?: string): void {
localStorage.setItem(TOKEN_KEY, accessToken);
if (refreshToken) {
localStorage.setItem(REFRESH_KEY, refreshToken);
}
}
/** Clear tokens on logout */
export function clearTokens(): void {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(REFRESH_KEY);
}
/** Get current access token */
export function getToken(): string | null {
return localStorage.getItem(TOKEN_KEY);
}
/** Check if the user is authenticated (has a non-expired token) */
export function isAuthenticated(): boolean {
const token = getToken();
if (!token) return false;
try {
const payload = JSON.parse(atob(token.split('.')[1]));
return payload.exp * 1000 > Date.now();
} catch {
return false;
}
}
/** Get the expiry timestamp (seconds since epoch) from a JWT, or null if undecodable */
export function getTokenExpiry(token: string): number | null {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
return typeof payload.exp === 'number' ? payload.exp : null;
} catch {
return null;
}
}
/** Check whether a JWT token is expired or within `marginSeconds` of expiry */
export function isTokenExpired(token: string, marginSeconds = 0): boolean {
const exp = getTokenExpiry(token);
if (exp === null) return true; // treat undecodable tokens as expired
return exp - marginSeconds <= Date.now() / 1000;
}
/** Session-expired flag key for sessionStorage */
export const SESSION_EXPIRED_KEY = 'session_expired';
/** Decode JWT claims without verification (for display purposes) */
export function getTokenClaims(): { sub: string; roles: string[] } | null {
const token = getToken();
if (!token) return null;
try {
return JSON.parse(atob(token.split('.')[1]));
} catch {
return null;
}
}