Commit Graph

7 Commits

Author SHA1 Message Date
MechaCat02
9364cb624a fix(dos): bound uploads, offload bcrypt, cap image decode, split queues
H3: re-enable a request-body limit (550MB, sized to the largest allowed video +
multipart overhead) instead of DefaultBodyLimit::disable(), and drop the
second full-file copy by keeping the upload as Bytes.

H4: image decode now goes through ImageReader with explicit dimension
(12000x12000) and allocation caps, rejecting decompression bombs before the
expensive resize, and the whole spawn_blocking is wrapped in a 120s timeout
mirroring the ffmpeg guard.

H8: every bcrypt hash/verify (join, recover, admin_login, host PIN reset) now
runs via spawn_blocking through a new services::password helper, so concurrent
auths can't pin the async workers.

M7: images and videos get independent compression permit pools, so slow videos
can no longer starve image-preview generation.

H5: add a generous per-IP daily account-creation cap (NAT-safe at ~100 guests)
on top of the burst cap, and wire up the previously-dead upload_count_quota_*
config as an event-wide file-count cap that a re-join cannot reset.

Also bumps generated recovery PINs from 4 to 6 digits (part of M4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 15:38:59 +02:00
MechaCat02
8faf702208 feat(media): authenticated signed media gateway (C1, C2-sink, C3, H2, H10)
Replaces the DB-blind `/media` ServeDir with a signed, DB-aware gateway at
`GET /media/{kind}/{id}`. Every media byte now flows through an HMAC-SHA256
signature check (minted into feed/upload DTOs for authenticated members; <img>
can't carry a Bearer header) plus a DB lookup:

- C1: export ZIP/HTML have no upload row, so they are unreachable by path —
  download stays behind the authenticated /export endpoints.
- C2 (sink): responses carry X-Content-Type-Options: nosniff and a locked-down
  CSP (default-src 'none'; sandbox), neutralizing any active content.
- C3 / H2: find_by_id filters deleted_at and the handler rejects ban-hidden
  uploaders, so deleted and moderated artifacts 404 — and the unauthenticated
  get_original alias (the H2 hole) is removed entirely.
- H10: delete paths (owner + host) now unlink original/preview/thumbnail after
  commit; soft_delete returns the paths; an hourly reaper reclaims disk for
  rows soft-deleted past a 1-day grace and hard-deletes them (FKs cascade).

Signed URLs are bucketed to a 1h window so they stay stable across feed polls
(browser cache hits) while expiring within 24h. media_token sign/verify has a
unit test (roundtrip + tamper + expiry).

Frontend: FeedUpload/pickMediaUrl now use the backend-provided signed
original_url; no client constructs a media path anymore.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 15:30:30 +02:00
MechaCat02
2068e8c1f3 fix(auth): reconcile role/ban from DB, revoke sessions, fix pin/comment bugs
H1: AuthUser now JOINs session→user and sources role/ban/event from the live
DB row instead of trusting JWT claims. Bans take effect on the next request;
demotion/promotion is immediate. Adds Session::find_auth_context and rejects
tokens whose sub/event_id disagree with the session row. This also closes M2
(banned export) and M3 (banned edit/delete) globally — banned users can no
longer pass the AuthUser extractor.

Adds Session::delete_by_user_id and revokes all of a user's sessions on
ban_user, set_role, and reset_user_pin so existing JWTs die immediately.
ban_user also emits a user-banned SSE for forced client logout.

H9: reset_user_pin used a non-existent column pin_failed_attempts (runtime
sqlx, so it 500'd in prod). Corrected to failed_pin_attempts — the only
account-recovery path now works.

C4: LightboxModal posted comments to /comment (singular); backend only
registers /comments. One-character route fix re-enables the comment feature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 15:14:22 +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
2e98f5ddf5 backend(features): quota enforcement, PIN reset, /me, original download, toggles
- handlers/me.rs (new): GET /api/v1/me/context (profile + role + privacy_note
  + quota toggle state, fetched once on app bootstrap) and GET /api/v1/me/quota
  (live used / limit / active uploaders / free disk).
- handlers/upload.rs:
  - quota enforcement via the dynamic formula
    floor((free_disk * tolerance) / max(active_uploaders, 1)),
    gated by quota_enabled + storage_quota_enabled toggles
  - new GET /api/v1/upload/{id}/original — unauthed by design
    (matches /media/previews/* — URL is the secret) so it works as
    <img src> / <video src> / window.open
  - rate-limit toggle wiring (rate_limits_enabled + upload_rate_enabled)
- handlers/host.rs:
  - POST /api/v1/host/users/{id}/pin-reset — Host may reset guest PINs,
    Admin may reset guest + host PINs (never another admin or self).
    Returns the freshly-generated plaintext PIN once; emits a global
    pin-reset SSE so the affected user's device can clear its localStorage.
  - set_role guard expanded so hosts can demote other hosts (not self,
    never admins) — backend match for the doc'd permission model.
- handlers/admin.rs: ALLOWED_KEYS split into NUMERIC_KEYS / BOOL_KEYS /
  TEXT_KEYS with per-kind validation; saving privacy_note broadcasts an
  event-updated SSE so other clients refresh live.
- handlers/feed.rs, handlers/admin.rs (export), auth/handlers.rs:
  rate-limit toggle wiring at every limiter call site.
- auth/handlers.rs: when an expired PIN lockout is detected on /recover,
  reset failed_pin_attempts to zero before the bcrypt check — without
  this every wrong PIN re-locked the user after the cooldown.
- main.rs: wire startup_recovery + spawn_periodic_tasks, register the
  new /me/context, /me/quota, /upload/{id}/original, and
  /host/users/{id}/pin-reset routes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 14:32:05 +02:00
MechaCat02
258e2bd84d feat: implement export engine
Add async ZIP and HTML offline viewer export workers, download endpoints,
and a guest-facing /export page.

Backend — export workers (tokio::spawn, run after gallery release):
- ZIP worker: streams all non-deleted originals into Gallery.zip via
  async_zip (Stored compression), organised into Photos/ and Videos/
  with {date}_{uploader}_{id}.{ext} filenames; updates progress_pct in DB
- HTML worker: renders Memories.html via minijinja template (self-contained:
  inlined CSS + JS, relative media paths); packs it with README.txt and
  all media into Memories.zip (Deflate for text, Stored for media)
- Both workers mark export_job status (running → done/failed), update
  export_zip_ready / export_html_ready on the event, and broadcast SSE
  export-progress + export-available when both complete

Backend — new endpoints (AuthUser):
- GET /export/zip   → streams Gallery.zip if export_zip_ready
- GET /export/html  → streams Memories.zip if export_html_ready
- GET /export/status → released flag + per-type status/progress (moved from admin)

Memories.html features: warm keepsake aesthetic, responsive grid, fullscreen
lightbox with captions/comments/likes, client-side hashtag filter chips,
XSS-safe JS, fully offline (no external deps)

Frontend — /export page:
- Locked state: padlock illustration + message
- Released state: ZIP and HTML cards with progress bars (SSE-driven),
  download buttons enabled only when ready
- HTML guide modal (unzip instructions + Wi-Fi tip) before download begins

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 20:56:21 +02:00
MechaCat02
71a2987a3e feat: implement host dashboard
Add Host Dashboard for event and guest management, accessible at /host.

Backend — new /api/v1/host/* endpoints (RequireHost auth):
- GET  /host/event                    → event name + lock/release state
- POST /host/event/close|open         → lock or unlock uploads; SSE broadcast
- POST /host/gallery/release          → set release timestamp, enqueue export jobs
- GET  /host/users                    → all guests with upload count & bytes
- POST /host/users/{id}/ban           → ban with optional upload-hide choice
- POST /host/users/{id}/unban         → lift ban
- PATCH /host/users/{id}/role         → promote guest→host or demote host→guest
- DELETE /host/upload/{id}            → host-level soft-delete + SSE
- DELETE /host/comment/{id}           → host-level soft-delete

Frontend — /host page:
- Event controls: lock/unlock toggle and release-gallery button with status badges
- Guest table: display name, role badge, upload count, storage used
- Ban flow: modal asking whether to keep or hide the user's uploads
- Promote/demote buttons respecting caller role (host can promote guests; admin can demote hosts)
- auth.ts: getRole() decodes JWT payload client-side to gate the route

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 20:43:09 +02:00