Commit Graph

8 Commits

Author SHA1 Message Date
fabi
36fe59caa5 fix(user-flow): persona-audit fixes — ban replay, locked-upload data loss, host UX, admin session
Follows the perf + security + user-flow work with a role/persona audit (guest, host,
admin, projector) and fixes across three review rounds. Highlights:

HIGH
- Ban now replays on reconnect. A ban isn't a soft-delete, and the `user-hidden` SSE has
  no replay, so a client that missed it (esp. the unattended diashow) kept cycling a
  banned user's slides. New `uploads_hidden_at` (migration 013) + `hidden_user_ids` in
  /feed/delta; feed + diashow evict those users. Applied even on a truncated delta.

MEDIUM
- Locked-upload data loss: a photo staged offline during a lock/release was purged as a
  terminal 4xx and lost when the host reopened. New reversible `uploads_locked` error code;
  the queue keeps the blob and auto-resumes on the `event-opened` SSE.
- Reopen after release now warns (ConfirmSheet) that it revokes the published keepsake.
- Host "forgotten-PIN" badge updates live (`pin-reset-requested` was broadcast but never
  in KNOWN_EVENTS / subscribed); host page also refetches on `pin-reset` so a two-host
  race can't hand out a conflicting PIN.
- Ban modal copy fixed (read-only ban, not "session ended"); Degradieren/Sperren/Entsperren
  hidden on peer-host rows for non-admins (they always 403'd).
- Host dashboard shows live keepsake generation progress / ready state + link to /export.
- Admin JWT moved to sessionStorage (§11.1) to bound exposure on shared devices.

Export generation guard (H1 from the prior round, hardened): per-(event,type) `release_seq`
(migration 012) with seq-guarded claim/finalize/mark_failed/update_progress, per-generation
temp/final paths, download follows `file_path`, prune only strictly-older generations.

LOW: diashow coalesces upload-processed (avoids self-rate-limit); event-closed reconciles
galleryReleased; /recover gains a forgot-PIN request + drops a stale cached PIN on 401;
delta `>=` tie-break + 429 retry; misc copy/labels.

Adds e2e: ban-replay, upload-lock-code, and rewrites export-reopen-rerelease with a
data-completeness test. Reconciles USER_JOURNEYS §9/§11.

Verified: cargo build clean, 40 unit tests, svelte-check 0 errors, 33 frontend unit
tests, 155 e2e passing on chromium-desktop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 21:06:28 +02:00
MechaCat02
641174717c fix(user-flow): close offline-queue, export, session, ban & realtime gaps
Some checks failed
E2E / Playwright E2E (chromium-desktop) (push) Failing after 6m52s
E2E / Cross-UA smoke matrix (push) Failing after 3m50s
Comprehensive user-flow review across guest/host/admin roles, then fixes with
e2e regression guards. Highlights:

- Offline upload queue auto-resumes on reconnect: network errors keep items
  pending (not error), 4xx are terminal (no infinite retry), quota exhaustion
  returns a distinct 413; queue cap + dedup.
- Export lifecycle: release atomically locks uploads; reopen invalidates and
  re-release regenerates the keepsake; workers claim their job atomically so a
  reopen->re-release can't corrupt the ZIP; startup re-spawns interrupted
  exports.
- Sessions slide on activity (no 30-day cliff); JWT expiry deferred to the
  revocable session row; sign-out-everywhere + revoke-on-PIN-reset.
- Ban always hides content (v_feed / find_visible_media / export filter
  is_banned) but stays a read-only ban per USER_JOURNEYS §10 — sessions are
  not revoked, read access + keepsake download preserved.
- Realtime: server-clock SSE delta cursor; event-closed/opened drive the UI
  live; feed_delta rate-limited; like returns {liked, like_count} to fix
  multi-device drift; lightbox live comments; diashow delta backfill.
- Forgotten-PIN in-app request flow; simultaneous same-name join returns 409;
  quota increment is transactional; operator floor.

Adds e2e/specs/10-flow-review/ (offline resume, export integrity, deterministic
anti-race guard) and updates existing specs for the new contracts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 23:05:37 +02:00
fabi
d6c91974eb perf(backend): close config/upload/export hot paths + stability fixes
Performance:
- Cache the runtime `config` table in-memory (ConfigCache) with synchronous
  invalidation on every write (admin PATCH + test reseed). Was re-reading each
  key from Postgres on every request (~8 round-trips per upload).
- Stream uploads chunk-by-chunk to a temp file instead of buffering the whole
  body in RAM (peak was up to the per-class cap, e.g. 500 MB/video); only 512
  sniff-bytes are kept for magic-byte detection, then atomic rename into place.
- Cache the media-filesystem disk snapshot (DiskCache, 15s TTL) shared by the
  quota check and admin stats; drop the discarded System::refresh_all().
- HTML export streams video (and small-image) originals straight into the ZIP
  via a manifest instead of copying them to a temp dir first (removed the
  transient 2x disk usage) and drops the double directory scan.
- Auth extractor resolves session -> live user in one JOIN (was two queries),
  touching last_seen_at by token hash.

Stability:
- SSE: on broadcast lag, emit a `resync` event so the client runs a delta
  fetch instead of silently losing events; frontend reconciles adds, deletions,
  and (via an in-place refresh) like/comment counts on visible cards.
- Storage quota fails OPEN when the disk can't be read (was a 0-byte limit that
  locked out all uploads).
- Graceful shutdown drains in-flight requests on SIGTERM/SIGINT, bounded by a
  10s backstop so open SSE streams can't stall a deploy.
- Upload removes the persisted file if the DB transaction fails (no orphaned
  bytes with no row to reclaim them).

Tests:
- New pure select_disk() with 5 unit tests (longest-prefix, fallbacks, fail-open).
- New e2e export-video spec covering the HTML export's video-streaming branch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 20:26:45 +02:00
fabi
0ed97f45cf fix(review-2): address re-review — close two blocker regressions + harden tests
The round-2 re-review (full suite green) found two real defects the passing
tests missed, both now fixed and covered:

B1 — banned users could still edit/delete their OWN content: edit_upload,
     delete_upload, delete_comment skipped the is_banned guard that upload/
     like/comment got (round-2 removed the global extractor block and missed
     these three). Added the guard (using auth.is_banned — no extra query).
     The moderation ban test now asserts 403 on DELETE upload, PATCH upload,
     and DELETE comment for a banned owner (+ upload survives) — it previously
     only exercised POST /upload, which is why the gap shipped green.

B2 — H3 live-eviction was dead code: 'user-hidden' and 'comment-deleted' were
     missing from KNOWN_EVENTS, so EventSource never listened and the handlers
     never fired. Added both. New browser-driven sse-eviction test asserts a
     hidden user's card actually evicts from an open feed with no reload — the
     backend-only SseListener checks couldn't catch this.

Minor items folded in:
- admin dashboard bounces a mid-session-demoted admin to /feed via live
  /me/context (mirrors the host page) instead of a dead error screen.
- feed "new posts" pill cleared on any full refresh (pull-to-refresh no longer
  strands it).
- corrected the stale sse.rs comment (banned users may hold a read-only stream).

Verified: backend 35 unit tests; svelte-check 0 errors; e2e 04-host + comment-ui
+ export-leak 13/13 on chromium (incl. the two new regression tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 22:45:57 +02:00
fabi
91ff961850 fix(security): medium/low — event-lock, atomic config, a11y, diashow, hygiene
- social: likes/comments rejected on a closed event (e2e proven).
- admin: patch_config validates-then-writes atomically; char-based length.
- hashtag/comment models: atomic tag writes in edit + comment paths.
- Modal: inert the background (modal-inert action) for screen readers.
- sse.ts: idempotent visibilitychange listener (no duplicate reconnects).
- diashow: videos advance on `ended` (transitions + page wiring).
- docs/hygiene: README clone-case fix; removed stale committed .env.test and
  gitignored it; docker-compose.dev.yml tidy.

Left documented as accepted-risk per plan: PIN-persist-after-logout,
UUID-reachable hidden uploads, DECISION-media-auth.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 21:25:58 +02:00
MechaCat02
d228676a56 fix(security): cross-event authz, SSE ticket flow, account hardening, audit logs
Follow-up to the comprehensive code review. Five batches:

1. Cross-event authorization: host_delete_upload, unban_user, and
   host_delete_comment now scope by auth.event_id. Adds
   Upload::find_by_id_and_event / soft_delete_in_event and a
   Comment::soft_delete_in_event variant that joins through upload.

2. Token exposure: SSE auth no longer puts the JWT in the URL.
   New /api/v1/stream/ticket endpoint mints a short-lived single-use
   ticket bound to the session; the EventSource passes ?ticket=...
   instead. Refuse to start in APP_ENV=production with the dev JWT
   sentinel; warn loudly otherwise.

3. Account hardening: per-IP+name rate limit on /recover (mitigates
   targeted lockout DoS), per-IP rate limit on /admin/login, random
   32-char admin recovery PIN (replaces "0000"), structured tracing
   events for wrong PIN, lockout, failed admin login, ban/unban/role
   change/pin-reset/host-delete.

4. DoS / correctness: comment listing paginated (LIMIT 50 + ?before=
   cursor), hashtag extraction whitelisted to ASCII alnum+underscore
   (≤40 chars) with unit tests, display_name / caption / comment body
   length validated in chars rather than bytes.

5. Cleanup: session-touch failures now logged, DATABASE_MAX_CONNECTIONS
   env var (default 10).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:00:51 +02:00
MechaCat02
251f9f1469 frontend(plumbing): shared theme tokens, cross-cutting stores, gestures, sheets
The plumbing layer the v0.16 UI features (and dark mode) build on.

Shared design tokens (Tailwind v4):
- tailwind-theme.css (new): @custom-variant dark (class-driven, beats OS
  default) + @theme color/font/radius tokens + baseline html/html.dark
  rules so any page that hasn't been re-themed still renders the right
  body bg + color-scheme.
- src/app.css + export-viewer/src/app.css now import the shared theme.
- src/app.html: 6-line FOUC guard sets <html class="dark"> before paint
  (mirrored from theme-store.ts) so dark reloads no longer flash white.
  Adds <meta name="theme-color"> kept in sync by initTheme().

Cross-cutting stores (one per concern, per docs/FEATURES §2.9):
- data-mode-store.ts: 'saver' | 'original' per-device, plus pickMediaUrl
  helper so feed cards / lightbox / diashow all resolve URLs the same way.
- privacy-note-store.ts: hydrated from /me/context, refreshed on SSE
  event-updated.
- quota-store.ts: { enabled, used, limit, active_uploaders, free_disk },
  refreshed after each upload completes.
- theme-store.ts: 'system' | 'light' | 'dark' preference + derived
  appliedTheme + initTheme() that syncs <html class>, localStorage,
  and the theme-color meta. Listens to prefers-color-scheme.
- auth.ts: currentPin writable mirror + clearPin() helper called from
  the global pin-reset SSE handler — fixes the stale-PIN bug where the
  localStorage copy survived a reset.

DTO mirror:
- types.ts: QuotaDto, MeContextDto, PinResetResponse, DeltaResponse each
  carry a `// mirrors backend/...` comment per the lib README convention.

SSE client:
- sse.ts: KNOWN_EVENTS registry (one entry per server-emitted type),
  synthetic feed-delta dispatched after foreground reconnect via the
  /feed/delta?since= endpoint, exponential backoff (1 → 60 s + jitter)
  on errors, attempt counter reset on user-initiated visibility resume.

Upload queue:
- upload-queue.ts: IDB schema bumped to v2 — entries tagged with userId;
  loadQueue filters by current user (no cross-user leak on shared
  devices); uploadItem refuses to upload an entry whose userId differs
  from getUserId() (defense-in-depth); new clearQueue() called on
  explicit logout. v2 upgrade wipes pre-v2 entries (no userId, can't
  attribute safely).

Mobile primitives:
- actions/longpress.ts: 500 ms hold with 10 px move tolerance, swallows
  the next click + the right-click contextmenu so the gesture doesn't
  double-fire the inner button's onclick.
- actions/doubletap.ts: tap-pair detector that preventDefaults the
  second tap so iOS Safari doesn't also zoom on double-tap.
- components/ContextSheet.svelte: generic bottom sheet driven by a
  ContextAction[] prop. Reused by feed posts, comments, host user rows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 14:32:37 +02:00
fabi
964598e41d feat: implement gallery feed with social features and SSE
- Cursor-based feed endpoint using v_feed view with hashtag filtering
- Like toggle (INSERT ON CONFLICT), comments CRUD
- Feed delta endpoint for SSE-driven incremental updates
- SSE client with Page Visibility API (pause/reconnect)
- Responsive photo/video grid with infinite scroll
- Hashtag filter chips, lightbox modal with comments
- Media file serving via tower-http ServeDir

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 19:17:06 +02:00