fix(audit-followup): close regressions/gaps from the persona-audit re-review
Adversarial re-review of the persona-audit commit surfaced two HIGH regressions I'd introduced plus MED/LOW gaps. All fixed: HIGH - Auth privilege escalation: reads are sessionStorage-first, but guest `setAuth` wrote only localStorage — a resident admin token shadowed a new guest login (guest ran as admin on a shared device). setAuth/setAdminAuth now DISPLACE the other store so exactly one identity is resident. Adds unit + e2e regression guards. - Host dashboard: `exportGenerating` used `||`, so a one-half export failure stuck on "wird erstellt…" forever and never showed the re-release hint. Changed to `&&`. MEDIUM - Locked-upload auto-resume was unreliable (`event-opened` has no reconnect replay and SSE is down on non-feed pages) — now also resumes on `feed-delta`, which fires on every SSE reconnect. - Queue-cap eviction could drop a recoverable locked item (parked as `error` with a live blob) — now evicts only `done`/`blocked`. - Host page async-`onMount` leaked SSE handlers if unmounted mid-load — added a `destroyed` guard before registration. LOW - An unparseable 403 body now keeps the blob (reversible) instead of purging it. - `refreshEventState` is sequence-guarded so a late close-refresh can't clobber a reopen. - `/export/status` moved out of the all-or-nothing `reload()` so its failure can't blank the whole host dashboard. Verified: svelte-check 0 errors, 36 frontend unit tests (incl. new displacement guards). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -56,9 +56,12 @@
|
||||
let exportReady = $derived(
|
||||
exportInfo?.zip?.status === 'done' && exportInfo?.html?.status === 'done'
|
||||
);
|
||||
// The keepsake needs BOTH halves, so EITHER failing means the whole thing failed — use
|
||||
// `&&` (not `||`), else a one-half failure stays stuck on "wird erstellt…" forever and the
|
||||
// host never sees the "re-release" recovery hint.
|
||||
let exportGenerating = $derived(
|
||||
!!exportInfo?.released && !exportReady &&
|
||||
(exportInfo?.zip?.status !== 'failed' || exportInfo?.html?.status !== 'failed')
|
||||
exportInfo?.zip?.status !== 'failed' && exportInfo?.html?.status !== 'failed'
|
||||
);
|
||||
let exportProgress = $derived(
|
||||
Math.min(exportInfo?.zip?.progress_pct ?? 0, exportInfo?.html?.progress_pct ?? 0)
|
||||
@@ -148,6 +151,12 @@
|
||||
}
|
||||
await reload();
|
||||
|
||||
// The awaits above mean the component can already be destroyed by the time we get
|
||||
// here (user navigated away mid-load). Svelte doesn't cancel an async onMount, so
|
||||
// without this guard onDestroy would have run with an empty `sseOff` and we'd leak
|
||||
// these handlers into the module-global SSE map forever.
|
||||
if (destroyed) return;
|
||||
|
||||
// Live updates so the dashboard doesn't need a manual reload:
|
||||
// - pin-reset-requested: a guest just asked for a reset → refresh the badge/list.
|
||||
// - pin-reset: some host resolved a reset → drop it from the list (two-host race:
|
||||
@@ -161,7 +170,9 @@
|
||||
];
|
||||
});
|
||||
|
||||
let destroyed = false;
|
||||
onDestroy(() => {
|
||||
destroyed = true;
|
||||
for (const off of sseOff) off();
|
||||
});
|
||||
|
||||
@@ -169,17 +180,20 @@
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
[event, users, pinResetRequests, exportInfo] = await Promise.all([
|
||||
[event, users, pinResetRequests] = await Promise.all([
|
||||
api.get<EventStatus>('/host/event'),
|
||||
api.get<UserSummary[]>('/host/users'),
|
||||
api.get<PinResetRequest[]>('/host/pin-reset-requests'),
|
||||
api.get<ExportStatusDto>('/export/status')
|
||||
api.get<PinResetRequest[]>('/host/pin-reset-requests')
|
||||
]);
|
||||
} catch (e: unknown) {
|
||||
error = e instanceof Error ? e.message : 'Fehler beim Laden.';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
// Export status is a secondary widget — fetch it OUTSIDE the all-or-nothing block above
|
||||
// (and error-swallowing) so a transient /export/status failure can't blank the whole
|
||||
// dashboard (users, moderation, settings).
|
||||
void refreshExportStatus();
|
||||
}
|
||||
|
||||
/** Refetch just the pending PIN-reset requests (live badge; avoids a full-page reload). */
|
||||
|
||||
Reference in New Issue
Block a user