This commit is contained in:
2026-09-24 11:42:02 +05:30
parent f3fe53d2ac
commit beeee893f6
2 changed files with 55 additions and 1 deletions

View File

@@ -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})`,

View File

@@ -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.
}
}