fix(mana/web/who): surface guestPrompt on JWT expiry

When the access token had aged out mid-game and the silent refresh
failed (auth.mana.how/api/v1/auth/refresh → 401), the who store
threw a raw "not authenticated" error and the PlayView showed a
gibberish red banner. Confusing because the navbar still shows the
user as logged in — the session cookie is intact, only the JWT is
gone — so the user has no clue what to do.

Match the base-client.ts pattern: when getAccessToken() returns
null OR the upstream returns 401, fire guestPrompt.requireAccount()
to surface the standard "Sitzung abgelaufen, neu anmelden" prompt
in the bottom-bar slot, then throw a German error string so the
inline error banner reads as "Sitzung abgelaufen — bitte neu
anmelden" instead of "not authenticated".

Hit by the developer mid-test on the first end-to-end live game on
production: the chat had been working for ~5 messages, then the
JWT expired and the game appeared to "die" with a cryptic message.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-09 17:10:03 +02:00
parent f9b83990c6
commit 59b5114348

View file

@ -15,6 +15,7 @@
import { db } from '$lib/data/database';
import { authStore } from '$lib/stores/auth.svelte';
import { guestPrompt } from '$lib/stores/guest-prompt.svelte';
import { encryptRecord } from '$lib/data/crypto';
import { whoGameTable, whoMessageTable } from '../collections';
import type {
@ -37,10 +38,25 @@ const API_BASE = '/api/v1/who';
* this app Bearer token from authStore, JSON body, structured
* error throwing. Kept inline (no wrapping client) because the who
* module has only three endpoints; a full client would be overkill.
*
* 401 handling: when the access token is missing OR the upstream
* returns 401, we surface the standard `guestPrompt.requireAccount`
* UI instead of throwing a raw "not authenticated" error. The
* common case is JWT expiry mid-game the session cookie is still
* present (so the navbar still shows the user as logged in) but
* the access token has aged out and the silent refresh failed.
* Same pattern as base-client.ts uses for every other API call.
*/
async function postJson<T>(path: string, body: unknown): Promise<T> {
const token = await authStore.getAccessToken();
if (!token) throw new Error('not authenticated');
if (!token) {
guestPrompt.requireAccount(
'who',
'Sitzung abgelaufen — bitte neu anmelden, um weiterzuspielen.',
'Neu anmelden'
);
throw new Error('Sitzung abgelaufen — bitte neu anmelden');
}
const res = await fetch(`${API_BASE}${path}`, {
method: 'POST',
headers: {
@ -49,6 +65,14 @@ async function postJson<T>(path: string, body: unknown): Promise<T> {
},
body: JSON.stringify(body),
});
if (res.status === 401) {
guestPrompt.requireAccount(
'who',
'Sitzung abgelaufen — bitte neu anmelden, um weiterzuspielen.',
'Neu anmelden'
);
throw new Error('Sitzung abgelaufen — bitte neu anmelden');
}
if (!res.ok) {
const text = await res.text().catch(() => '');
throw new Error(`who ${path} failed: ${res.status} ${text}`);