fix(frontend): three dead ends a guest cannot get out of
**1. The cached PIN could never be cleared after a host reset.** `/recover` clears a rejected cached PIN only when the submitted name is the one this device belongs to — narrowed on this branch so a guest who mistypes their own name does not lose the only copy of their PIN (the server keeps just the bcrypt). But it compared against `DISPLAY_NAME_KEY`, which `clearAuth` deletes for shared-device privacy — one step BEFORE the guest ever reaches that screen: host taps "PIN zurücksetzen" -> the backend also revokes every session for that user -> the guest's next request 401s -> clearAuth -> redirect to /join -> they go to /recover, where the field is pre-filled with the dead PIN and the guard can never fire again Since a 4-digit value auto-submits, every correction burns another of the four wrong-PIN attempts the shared venue IP allows per 15 minutes. The PIN's owner is now stored WITH the PIN and survives alongside it, with a fallback to the auth display name for devices that cached a PIN before this key existed. **2. Every layout-level SSE handler waited on the `/me/context` retry.** The retry was awaited inside the same `onMount` that registers `pin-reset`, `user-hidden`/`user-shown`, `event-closed`/`event-opened` and `event-updated`. Worst case is a 20s timeout + 2s backoff + a second 20s timeout: ~42s with an empty handler list, on exactly the wifi the retry exists for. Five of the six self-heal; `pin-reset` does not, and a missed one leaves a dead PIN displayed in "Mein Konto" and pre-filling /recover — the same state as (1), reached from the other end. Detached, since nothing below reads its result. **3. `crypto.randomUUID` was on the join critical path.** It needs Safari >= 15.4 / Chrome >= 92 AND a secure context. The queue already depended on it, so an old phone previously joined and browsed and only failed at upload — degraded but survivable. Minting an idempotency key at join turned that into a `TypeError` caught by the generic handler and rendered as "Ein Fehler ist aufgetreten." on every retry: cannot join, cannot browse, and /recover is no help because there is no account yet. The one screen where a hard failure has no way out at all. Falls back to `crypto.getRandomValues` with the RFC 4122 version and variant bits set; `Math.random` is deliberately NOT a further fallback, since a collision between two guests would replay one guest's join or upload onto another. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { goto, afterNavigate } from '$app/navigation';
|
||||
import { api, ApiError } from '$lib/api';
|
||||
import { setAuth, getPin, getToken, clearPin, getDisplayName } from '$lib/auth';
|
||||
import { setAuth, getPin, getToken, clearPin, getPinOwner } from '$lib/auth';
|
||||
import { markGuideSeen } from '$lib/onboarding';
|
||||
import { browser } from '$app/environment';
|
||||
import IconButton from '$lib/components/IconButton.svelte';
|
||||
@@ -95,8 +95,14 @@
|
||||
// So clear only when the evidence actually points at a stale cache: the name they
|
||||
// submitted is the one this device belongs to, AND the PIN that was rejected is the
|
||||
// cached one. Any other 401 leaves stored state untouched.
|
||||
// `getPinOwner()`, NOT `getDisplayName()`. A host PIN reset also revokes the guest's
|
||||
// sessions, so by the time they reach this screen `clearAuth` has already deleted the
|
||||
// display name — and the guard could never fire again on that device. The dead PIN
|
||||
// then pre-fills this field forever, and since a 4-digit value auto-submits, every
|
||||
// correction burns another of the four wrong-PIN attempts the shared venue IP allows
|
||||
// per 15 minutes. The PIN's owner is stored with the PIN and survives with it.
|
||||
const submittedOwnName =
|
||||
getDisplayName()?.trim().toLowerCase() === displayName.trim().toLowerCase();
|
||||
getPinOwner()?.trim().toLowerCase() === displayName.trim().toLowerCase();
|
||||
const submittedCachedPin = getPin() !== null && pin.trim() === getPin();
|
||||
if (e.status === 401 && submittedOwnName && submittedCachedPin) clearPin();
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user