feat(auth): use the cookie session and refresh on 401

The token is no longer stored; a tbp_session flag remembers the
login. apiFetch renews the session once on a 401 and sends the
request again. Parallel requests and other tabs share one refresh,
because Auth revokes the session when a refresh token is used twice.
Only a 401 from the refresh ends the session; a 502 or no network
shows the normal error.
This commit is contained in:
lobo committed 2026-09-25 13:45:08 +02:00
1 parent 5ff8d48c53
commit d9a39dda22
34 files changed
+297 -147

No files matched your search

+17 -27
View File
@@ -1,35 +1,25 @@
const TOKEN_KEY = 'tbp_jwt';
// The tokens live in HttpOnly cookies that no script can read. This flag only remembers that a login happened; a
// stale flag costs one 401 and a failed refresh, then the login page opens.
const SESSION_KEY = 'tbp_session';
// Where the token lived before the cookies. Removed on the next login or logout.
const LEGACY_TOKEN_KEY = 'tbp_jwt';
export const getToken = () => localStorage.getItem(TOKEN_KEY);
export const setToken = (token) => localStorage.setItem(TOKEN_KEY, token);
export const clearToken = () => localStorage.removeItem(TOKEN_KEY);
export const hasSession = () => localStorage.getItem(SESSION_KEY) !== null;
/**
* Only checks `exp`. The signature is checked by the backends.
* Changes on every login and on every settings save, so a hook that shows the user's Steam data can load again.
*/
export function isTokenValid(token, nowMs = Date.now()) {
try {
const payload = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
const {exp} = JSON.parse(atob(payload));
return typeof exp === 'number' && exp * 1000 > nowMs;
} catch {
return false;
}
}
export const sessionStamp = () => localStorage.getItem(SESSION_KEY);
/**
* Returns the stored token, or null. Removes an expired token.
*/
export function getValidToken() {
const token = getToken();
if (token && !isTokenValid(token)) {
clearToken();
return null;
}
return token;
}
export const markSession = () => {
localStorage.setItem(SESSION_KEY, String(Date.now()));
localStorage.removeItem(LEGACY_TOKEN_KEY);
};
export const clearSession = () => {
localStorage.removeItem(SESSION_KEY);
localStorage.removeItem(LEGACY_TOKEN_KEY);
};
/**
* Returns the path when it is a path inside this app, else null. The login page uses it to go back after a