fix(pwa): stop the offline cache from persisting sensitive health data

The offline layer wrote far too much to disk and cleared it too rarely.
The query persister used a denylist (only auth/session/admin/tokens), so
every other family — coach, insights, illness, labs, mood, cycle — was
dehydrated to IndexedDB in plaintext; the service worker likewise
prefix-cached GET /api/insights, which serves decrypted Coach
conversations. Neither store carried an account marker and both were
wiped only on explicit logout, so a session that ended by expiry or a
401 left health data on disk for the next account on a shared profile.

Flip the persister to a strict allowlist (dashboard snapshot, per-chart
series, widget layout — the offline-dashboard set only) and stamp each
payload with the live account id, discarding on mismatch. Drop
/api/insights and /api/analytics from the worker allowlist. Wipe the
IndexedDB blob and the data/page caches on every session end — the
401/expiry redirect and the logout path both route through one helper.
This commit is contained in:
Marc-André Bombeck committed 2026-06-18 18:18:24 +02:00
1 parent 3cf9a262ca
commit b474ea1489
7 files changed
+273 -59

No files matched your search

+8
View File
@@ -8,6 +8,7 @@ import { MaintainershipBanner } from "@/components/i18n/maintainership-banner";
import { LayoutCoachFab } from "@/components/insights/layout-coach-fab";
import { LayoutCoachMount } from "@/components/insights/layout-coach-mount";
import { useAuth } from "@/hooks/use-auth";
import { clearOfflineCachesForSessionEnd } from "@/lib/pwa/query-persister";
import { useTranslations } from "@/lib/i18n/context";
import { CoachLaunchProvider } from "@/lib/insights/coach-launch-context";
import { BottomNav } from "./bottom-nav";
@@ -91,6 +92,13 @@ export function AuthShell({
useEffect(() => {
if (!isLoading && !isAuthenticated && !isPublicPage) {
// Session END (expired / invalidated cookie → `/api/auth/me` 401). Wipe
// every client-side cache that can hold the previous account's health
// data before bouncing to login, so it never leaks to the next account
// on a shared device. Logout does the same from `useLogout`; this covers
// the expiry path that logout never runs through. Best-effort; the
// redirect proceeds regardless.
void clearOfflineCachesForSessionEnd();
router.replace("/auth/login");
}
}, [isLoading, isAuthenticated, isPublicPage, router]);