changes
This commit is contained in:
@@ -6,9 +6,38 @@
|
||||
* changes. Nothing else in the app calls `fetch`.
|
||||
*/
|
||||
|
||||
import { authHeader } from '@/auth/token';
|
||||
import { authHeader, forgetSession, readSessionToken } from '@/auth/token';
|
||||
import type { FiestaEnvelope } from './types';
|
||||
|
||||
/**
|
||||
* A 401 on a call we authenticated means the session is over.
|
||||
*
|
||||
* Twelve hours after signing in, or the moment the signing key is rotated under
|
||||
* an open tab, every request starts coming back 401. Without this the console
|
||||
* keeps sending the dead token and each page renders its own error — which a
|
||||
* shopkeeper reads as "my data has gone", not as "sign in again". The screen
|
||||
* fills with failures and nothing tells them the one thing that would fix it.
|
||||
*
|
||||
* Only when a token was actually SENT. A 401 on an anonymous call is the
|
||||
* server declining to serve a stranger, not a session ending — and the
|
||||
* sign-in probe deliberately posts with no password to read a 401 back, so
|
||||
* reacting to that one would clear the session at the login screen and make
|
||||
* signing in impossible.
|
||||
*
|
||||
* `location.reload()` rather than a router push: the session is held in React
|
||||
* state that this module cannot reach, and a reload is the one move guaranteed
|
||||
* to land on the sign-in screen from anywhere in the app. It happens once,
|
||||
* because the storage is cleared first — the reloaded app has no token, so the
|
||||
* next 401 cannot loop.
|
||||
*/
|
||||
function endDeadSession(path: string): void {
|
||||
if (!readSessionToken()) return;
|
||||
forgetSession();
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`[nearle] session rejected on ${path}; signing out`);
|
||||
if (typeof window !== 'undefined') window.location.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Where Fiesta is.
|
||||
*
|
||||
@@ -182,6 +211,10 @@ async function request<T>(path: string, options: RequestOptions = {}): Promise<T
|
||||
throw new FiestaError(`Malformed response (HTTP ${response.status})`, response.status, path);
|
||||
}
|
||||
|
||||
if (response.status === 401) {
|
||||
endDeadSession(path);
|
||||
}
|
||||
|
||||
if (!response.ok || envelope.status === false) {
|
||||
throw new FiestaError(
|
||||
envelope.message ?? `Request failed (HTTP ${response.status})`,
|
||||
|
||||
@@ -58,3 +58,24 @@ export function authHeader(): Record<string, string> {
|
||||
const token = readSessionToken();
|
||||
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop this tab's session, because the server says the token is no longer good.
|
||||
*
|
||||
* A session expires after twelve hours, and the signing key can be rotated
|
||||
* under a tab that is still open. Without this the console goes on sending a
|
||||
* dead token and every page shows an error — which reads to the person as "my
|
||||
* data has gone", not as "sign in again".
|
||||
*
|
||||
* Lives here rather than in `session.ts` for the same reason the reader does:
|
||||
* the HTTP client is what learns a session is dead, and it cannot import the
|
||||
* module that calls it.
|
||||
*/
|
||||
export function forgetSession(): void {
|
||||
try {
|
||||
sessionStorage.removeItem(SESSION_STORAGE_KEY);
|
||||
} catch {
|
||||
// Storage throws where site data is blocked. Nothing to do: the next read
|
||||
// returns undefined either way, which is the state this wanted.
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user