83 lines
2.2 KiB
TypeScript
83 lines
2.2 KiB
TypeScript
/**
|
|
* Authentication Manager
|
|
*/
|
|
|
|
const REFRESH_URL = '/auth/refresh';
|
|
const PING_URL = '/auth/ping';
|
|
const TOKEN_EXPIRY_BUFFER_MS = 60 * 1000;
|
|
const PING_INTERVAL_MS = 60 * 1000;
|
|
|
|
let tokenExpiresAt: number | null = null;
|
|
let refreshPromise: Promise<boolean> | null = null;
|
|
|
|
export function recordExpiry(expiresIn: number | null | undefined): void {
|
|
tokenExpiresAt = expiresIn != null ? Date.now() + expiresIn * 1000 : null;
|
|
}
|
|
|
|
function isTokenExpiredOrExpiring(): boolean {
|
|
if (tokenExpiresAt === null) return false;
|
|
return Date.now() >= tokenExpiresAt - TOKEN_EXPIRY_BUFFER_MS;
|
|
}
|
|
|
|
async function refreshToken(): Promise<boolean> {
|
|
if (refreshPromise) return refreshPromise;
|
|
|
|
refreshPromise = fetch(REFRESH_URL, { method: 'POST', credentials: 'include' })
|
|
.then(async r => {
|
|
if (!r.ok) return false;
|
|
try {
|
|
const body = await r.clone().json();
|
|
recordExpiry(body?.expires_in);
|
|
} catch { /* ignore parse errors */ }
|
|
return true;
|
|
})
|
|
.catch(() => false)
|
|
.finally(() => { refreshPromise = null; });
|
|
|
|
return refreshPromise;
|
|
}
|
|
|
|
/**
|
|
* Ensures the token is fresh before a request fires.
|
|
*/
|
|
export async function ensureFreshToken(): Promise<void> {
|
|
if (isTokenExpiredOrExpiring()) {
|
|
await refreshToken();
|
|
}
|
|
}
|
|
|
|
export interface SessionMonitorOptions {
|
|
onLogout: () => void | Promise<void>;
|
|
}
|
|
|
|
export function createSessionMonitor(options: SessionMonitorOptions) {
|
|
let intervalId: ReturnType<typeof setInterval> | null = null;
|
|
|
|
async function ping(): Promise<void> {
|
|
// GET ping — no body, no CSRF needed, just confirm the session cookie is still valid
|
|
const response = await fetch(PING_URL, { credentials: 'include' }).catch(() => null);
|
|
|
|
if (!response?.ok) {
|
|
// Session may be expired — attempt a token refresh before giving up
|
|
const refreshed = await refreshToken();
|
|
if (!refreshed) {
|
|
stop();
|
|
await options.onLogout();
|
|
}
|
|
}
|
|
}
|
|
|
|
function start(): void {
|
|
if (intervalId !== null) return;
|
|
intervalId = setInterval(ping, PING_INTERVAL_MS);
|
|
}
|
|
|
|
function stop(): void {
|
|
if (intervalId === null) return;
|
|
clearInterval(intervalId);
|
|
intervalId = null;
|
|
}
|
|
|
|
return { start, stop };
|
|
}
|