- Add tower-http TraceLayer to Axum router for request logging
- Configure Vite dev server proxy for /api and /media to localhost:3000
- Add TEST_GUIDE.md for manual frontend testing steps
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the rate limit, ban, or event-lock check fired before the multipart
body was consumed, the HTTP keep-alive connection was left in a bad state.
The next request from the Vite proxy then received a connection reset,
causing cascading "Netzwerkfehler" / empty-body 500 responses.
Fix: call drain_multipart() before every early return in the upload
handler so the connection remains clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Axum's DefaultBodyLimit defaults to 2MB. Uploaded photos/videos exceed
this and were rejected with 400 before reaching the handler. Disable the
body limit on POST /api/v1/upload — size validation is already enforced
inside the handler via max_image_size_mb / max_video_size_mb config.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add #[serde(rename_all = "lowercase")] to UserRole enum so JWT payloads
contain lowercase role strings ("guest", "host", "admin") matching what
the frontend route guards and getRole() comparisons expect.
Without this, JWTs contained PascalCase ("Guest", "Host", "Admin")
causing hosts and admins to be redirected to /join instead of their
dashboards.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
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>
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>
- 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>
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>
Backend:
- AppConfig, AppError, AppState modules for shared infrastructure
- JWT creation/verification with HS256 (jsonwebtoken crate)
- Session management: SHA-256 token hashing, DB-backed sessions
- Auth middleware: AuthUser, RequireHost, RequireAdmin extractors
- POST /api/v1/join: name-only registration, 4-digit PIN + bcrypt hash
- POST /api/v1/recover: PIN-based recovery with 3-attempt lockout (15 min)
- POST /api/v1/admin/login: bcrypt password verification
- DELETE /api/v1/session: logout (session invalidation)
- Migration 006: user PIN lockout columns (failed_pin_attempts, pin_locked_until)
- Models: Event, User (with role enum), Session with all CRUD methods
Frontend:
- api.ts: typed fetch wrapper with automatic Bearer token injection
- auth.ts: JWT/PIN localStorage management with Svelte store
- /join: name entry form with PIN display modal and copy button
- /recover: name + PIN recovery form with saved PIN pre-fill
- /feed: placeholder gallery page with logout
- Root layout: auth initialization on mount
- Root page: redirect to /join or /feed based on auth state
All responses use German language strings as specified.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>