Commit Graph

9 Commits

Author SHA1 Message Date
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
de0e395a9e feat: auto-retry uploads when rate limited (v0.13.0)
Backend: rate limiter gains check_with_retry() returning seconds until
the next slot opens. Upload 429 responses include retry_after_secs in
JSON and a Retry-After header.

Frontend: upload queue catches 429 as RateLimitError, resets affected
item to pending, schedules processQueue() for the server-reported delay,
and shows a live countdown banner in the queue UI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 18:37:51 +02:00
MechaCat02
3dc69e6c6d fix: upload body limit, role case, and connection drain (v0.12.1)
- Disable Axum's 2 MB default body limit on the upload route so large
  photos/videos are accepted without HTTP 400
- Serialize UserRole as lowercase in JWT so the frontend role checks
  ('guest'/'host'/'admin') match correctly
- Drain multipart body before returning early upload errors (rate-limit,
  ban, event-lock) to keep the HTTP keep-alive connection clean and
  prevent cascading Netzwerkfehler / empty-500 responses
- Add TraceLayer for request logging and Vite dev proxy config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 18:14:12 +02:00
MechaCat02
989d88022a feat: implement rate limiting across all API endpoints
Add sliding-window in-memory RateLimiter service (Arc<Mutex<HashMap>>)
with per-IP and per-user-id limits on all public endpoint classes:
- POST /api/v1/join: 5/min per IP
- GET /api/v1/feed: configurable per IP (feed_rate_per_min, default 60)
- POST /api/v1/upload: configurable per user (upload_rate_per_hour, default 10)
- GET /api/v1/export/zip|html: configurable per IP (export_rate_per_day, default 3)
Limits are hot-reloadable via the config table. All 429 responses use
German error messages. Client IP is read from X-Forwarded-For (Caddy).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 21:03:59 +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
32c16da3e2 feat: implement admin dashboard
Add Admin Dashboard at /admin for server configuration, disk usage
monitoring, and export job status, plus a public export/status endpoint.

Backend — new /api/v1/admin/* endpoints (RequireAdmin auth):
- GET  /admin/stats           → user/upload/comment counts + disk usage
- GET  /admin/config          → all config key/value pairs
- PATCH /admin/config         → update any subset of config keys; validates
                                 key whitelist and numeric values
- GET  /admin/export/jobs     → export_job rows for the event

Backend — public (AuthUser) endpoint:
- GET  /export/status         → released flag + zip/html job status/progress

Frontend — /admin page:
- Stats grid: guest count, upload count, comment count
- Disk usage bar with GB/MB formatting; red ≥ 90%, amber ≥ 75%
- Config form: labelled numeric inputs for all eight config keys,
  sends only changed values on save
- Export jobs list: type label, status badge, progress bar for running jobs,
  error message if failed; manual refresh button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 20:45:37 +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
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