// Session: the short-lived access token lives in sessionStorage; the refresh token is an // httpOnly cookie that the browser sends to /auth/* by itself (script code can't read it). const API = '/api/v1'; const STORAGE_KEY = 'access'; // Refresh a bit before the expiry so that a request doesn't go out with a dying token const MARGIN_MS = 30_000; function stored() { try { return JSON.parse(sessionStorage.getItem(STORAGE_KEY)); } catch { return null; } } function store(tokens) { const access = { token: tokens.access_token, expiresAt: Date.now() + tokens.expires_in * 1000 }; try { sessionStorage.setItem(STORAGE_KEY, JSON.stringify(access)); } catch { // sessionStorage is unavailable: the token is then only valid for this call chain } return access; } export function clearSession() { try { sessionStorage.removeItem(STORAGE_KEY); } catch { // nothing to clear } } async function post(path, body) { return fetch(API + path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body), }); } /** Signs in; throws an Error with `status` (401: wrong email or password). */ export async function login(email, password) { const response = await post('/auth/login/', { email, password, client: 'web' }); if (!response.ok) { const error = new Error(`HTTP ${response.status}`); error.status = response.status; throw error; } store(await response.json()); } // One refresh at a time: the refresh token works once, two parallel requests would // make the second one look like a stolen token let refreshing = null; /** Exchanges the cookie for a new access token. false: the session is over, sign in again. */ export function refresh() { refreshing ??= (async () => { try { const response = await post('/auth/refresh/'); if (!response.ok) { clearSession(); return false; } store(await response.json()); return true; } finally { refreshing = null; } })(); return refreshing; } /** A valid access token, refreshed when needed; null when there is no session. */ export async function accessToken() { let access = stored(); if (!access || access.expiresAt - Date.now() < MARGIN_MS) { if (!(await refresh())) { return null; } access = stored(); } return access?.token ?? null; } export async function logout() { try { await post('/auth/logout/'); } finally { clearSession(); } } /** Never settles: stops the page code that follows while the browser goes to the login page */ export function redirectToLogin() { const next = location.pathname + location.search; location.href = `login.html?next=${encodeURIComponent(next)}`; return new Promise(() => {}); }