Commit Graph

10 Commits

Author SHA1 Message Date
fabi
ee554e7f38 style(backend): rustfmt the whole tree; gate cargo fmt --check in CI
The backend had never been run through rustfmt. Doing it in one mechanical pass (134 files)
so no future functional diff is buried under formatting churn, then gating `cargo fmt
--check` in checks.yml so it stays clean.

Formatting only — no logic, SQL, or behaviour changed. Verified after the reformat:
cargo test 56 passed, clippy --all-targets -D warnings clean, cargo fmt --check clean.

This is the deferred cleanup noted when CI's Format step was first left out.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 19:52:17 +02:00
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
a4b2c5bd1c fix(security): harden authz, media gating, recover, SSE, deps
Security audit follow-up. No Critical/High existed; these close the
Medium/Low findings.

F1 [Med] Host privilege boundary: a host could demote a peer host to
  guest and then ban / PIN-reset (→ account takeover via /recover) them,
  because the ban/pin-reset peer guards key off the target's *current*
  role. set_role now blocks a non-admin from demoting a host.

F2 [Med] Moderation now revokes preview/thumbnail access: they are served
  through visibility-checked aliases (/api/v1/upload/{id}/{preview,
  thumbnail}) that filter soft-deleted / ban-hidden uploads, and direct
  /media/previews|thumbnails is 404-blocked. Feed emits the gated URLs;
  Caddy keeps them privately cacheable (max-age=300, short so moderation
  revokes promptly — the live feed already evicts cards via SSE). Uses a
  lean find_visible_media() (paths+mime only) on the media hot path.

F3 [Med/Low] npm audit fix: svelte 5.55.1→5.56.4 (SSR-XSS advisories),
  devalue 5.6.4→5.8.1 (DoS). Prod deps: 0 vulnerabilities.

F4 [Low] /recover no longer leaks account existence: unknown display name
  returns the same 401 as a wrong PIN, and runs a dummy bcrypt verify so
  timing matches — closing the enumeration + timing oracle.

F5 [Low] SSE streams re-validate the session every ~60s and close once it
  is logged out / expired, instead of running until client disconnect.

F6 [Info] X-Content-Type-Options: nosniff set at the app layer on all
  media responses (defense-in-depth alongside the edge).

Tests: new e2e specs for F1 (host cannot demote peer host), F2 (preview
gated + 404 after delete + direct /media blocked), F4 (uniform 401).
40 backend unit tests + 182 e2e passing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 06:14:00 +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
41ac742af8 test+docs: tighten H1 assertions, cover secret/XFF edges, document boot & SSE-ban
Re-review follow-ups (non-blocking quality items):

Tests:
- moderation H1 specs now assert exact `→ 403` instead of bare .rejects.toThrow(),
  so a spurious 500 can no longer masquerade as "revocation worked".
- config: added case-insensitivity (upper/mixed-case placeholder) and the
  len==32/31 boundary cases for validate_secrets.
- rate_limiter: added the trailing-comma empty-entry case for client_ip (must
  fall back, not return "").
  (Backend unit tests: 35 pass.)

Docs:
- README: note that with APP_ENV=production a placeholder .env makes the app
  refuse to boot and Caddy wait unhealthy — reason is in `docker compose logs app`.
- SECURITY-BACKLOG + sse.rs comment: document that a mid-session ban does not
  tear down an already-open SSE stream (new tickets are blocked; low blast radius).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 07:14:33 +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
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
fabi
3f052a4f91 feat: implement upload pipeline with compression and SSE
Backend:
- POST /api/v1/upload: multipart file upload with caption + hashtags
  - Validates file size against DB config limits (image/video separate)
  - Checks user ban status and event upload lock
  - Saves original to disk under {media_path}/originals/{slug}/
  - Tracks user total_upload_bytes for quota enforcement
  - Extracts hashtags from caption text and explicit CSV field
  - Upserts hashtags and links them to uploads
- PATCH /api/v1/upload/{id}: edit caption and hashtags (owner only)
- DELETE /api/v1/upload/{id}: soft-delete (owner only)
- GET /api/v1/stream: SSE endpoint with 30s keepalive
  - Broadcasts new-upload events to all connected clients
  - Uses tokio broadcast channel for fan-out

Services:
- CompressionWorker: Tokio semaphore-bounded (concurrency=2) background processor
  - Images: resize to 800px wide JPEG preview via image crate
  - PNG originals: lossless compression via oxipng
  - Videos: ffmpeg thumbnail extraction (1 frame at 1s, scaled to 800px)
  - Updates upload record with preview_path/thumbnail_path on completion

Models:
- Upload with full CRUD (create, find, update caption, soft delete, set paths)
- Hashtag with upsert, link/unlink, extract_hashtags() text parser
- UploadDto for API serialization with like/comment counts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 21:48:59 +02:00