f1tness_parser/frontend/js/auth.js

101 lines
2.9 KiB
JavaScript

// 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(() => {});
}