fix(audit): restore broken upload pipeline + role-based E2E audit fixes

A comprehensive role-based E2E audit (guest/host/admin, across browser
sessions) surfaced one critical and several smaller issues; this addresses
them and hardens the tests that missed them.

Critical
- The client upload pipeline was fully broken: the IndexedDB v1->v2 upgrade
  opened a *new* transaction inside the upgrade callback, which throws during
  a version-change transaction and aborted the whole upgrade, leaving the
  queue object store uncreated -- so no UI upload ever fired. Reuse the
  version-change transaction the callback provides, and bump the DB to v3 with
  a contains() guard so installs already corrupted by the shipped bug
  self-heal on next load. Re-enabled the previously-fixme'd UI upload E2E test.

High / Medium
- Event lock is uploads-only again: likes, comments and browsing stay open
  while the event is locked (USER_JOURNEYS 9.3 / FEATURES) -- it was wrongly
  freezing social interaction. Updated the event-lock spec accordingly.
- get_original now excludes soft-deleted and ban-hidden uploads, and direct
  /media/originals/** serving is blocked, so a hidden user's originals can no
  longer be pulled by UUID (all originals go through the checked alias).
- The upload handler reads the file field with an early-abort size cap chosen
  from the declared content-type, instead of buffering the entire body before
  the size check.

Low
- unban_user mirrors the ban role guard (a host can no longer unban a
  host/admin banned by an admin).
- reset_user_pin's UPDATE is event-scoped.
- Admin login returns and stores a real identity (user_id + display name)
  instead of a blank session.
- The host user list no longer renders target-actions (ban/promote/demote/PIN)
  on the caller's own row, where the backend always rejected them.
- /diashow gains a client-side auth guard like the other protected routes.
- The join page shows the event name via a new public GET /api/v1/event.

Verified: backend cargo build clean, frontend svelte-check 0 errors, full
Playwright E2E suite 144 passed / 1 skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
fabi
2026-07-07 07:28:27 +02:00
parent 14c667c694
commit faf7a2504a
16 changed files with 220 additions and 62 deletions

View File

@@ -33,16 +33,23 @@ async function getDb(): Promise<IDBPDatabase> {
// v1 → v2: add `userId` index so each guest's queue is isolated on shared devices.
// Pre-existing entries (no userId) are dropped on upgrade; nothing useful was ever
// persisted across logouts before this version.
db = await openDB(DB_NAME, 2, {
upgrade(database, oldVersion) {
if (oldVersion < 1) {
// Version 3 self-heals installs corrupted by a shipped v1→v2 bug: that upgrade
// opened a *new* transaction inside the callback, which throws InvalidStateError
// ("A version change transaction is running") and aborts the whole upgrade —
// leaving some browsers at version 2 with NO 'queue' object store (so every queue
// write failed and no upload ever fired). Bumping to 3 re-runs this upgrade for
// those installs; the contains() guard recreates the missing store instead of
// assuming createObjectStore only ever runs on a brand-new DB.
db = await openDB(DB_NAME, 3, {
upgrade(database, oldVersion, _newVersion, transaction) {
if (!database.objectStoreNames.contains(STORE_NAME)) {
database.createObjectStore(STORE_NAME, { keyPath: 'id' });
}
if (oldVersion < 2) {
// Wipe any pre-v2 entries — they have no userId field and would belong
// to a now-indeterminate user. Safer to drop than to misattribute.
const tx = database.transaction(STORE_NAME, 'readwrite');
tx.objectStore(STORE_NAME).clear();
} else if (oldVersion < 2) {
// Existing v1 store: its entries predate the `userId` field, so drop them
// rather than misattribute them to whoever is signed in now. Reuse the
// active version-change transaction (never open a new one here — see above).
// Skipped when we just created the store, which is already empty.
transaction.objectStore(STORE_NAME).clear();
}
}
});