- 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>
67 lines
1.9 KiB
TypeScript
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;
|
|
}
|
|
}
|