From 59b51143480457888e8c32db2bdc12e33424ed03 Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 9 Apr 2026 17:10:03 +0200 Subject: [PATCH] fix(mana/web/who): surface guestPrompt on JWT expiry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../lib/modules/who/stores/games.svelte.ts | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/apps/mana/apps/web/src/lib/modules/who/stores/games.svelte.ts b/apps/mana/apps/web/src/lib/modules/who/stores/games.svelte.ts index 2a397c75a..3245ccfc6 100644 --- a/apps/mana/apps/web/src/lib/modules/who/stores/games.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/who/stores/games.svelte.ts @@ -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(path: string, body: unknown): Promise { 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(path: string, body: unknown): Promise { }, 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}`);