Files
EventSnap/README.md
MechaCat02 e42d8a92a1 feat(e2e): Playwright suite — 134 tests across 9 spec areas + UA matrix
Adds an end-to-end Playwright test suite under e2e/ that spins up an
isolated docker-compose stack (Postgres :55432, Caddy :3101, backend with
EVENTSNAP_TEST_MODE=1, SvelteKit adapter-node frontend) and exercises the
SvelteKit app against the real Rust backend.

Phase 1 — happy paths covering every documented USER_JOURNEYS.md flow:
  01-auth/      join, recover, admin login, leave event, PIN lockout
  02-upload/    gallery picker (API path), rate-limit + admin toggle
  03-feed/      like/comment SSE, filters, SSE reconnect on visibility
  04-host/      event lock API, ban/unban, promote
  05-admin/     config validation, foundational authz guards, stats
  06-export/    /export status + download stub
  __smoke/      cross-UA happy-path (runs on every UA project)

Phase 2 — adversarial + browser chaos:
  07-adversarial/  XSS payloads (6 × display name path), SQLi shapes,
                   length / encoding / RTL override / NUL byte;
                   file-upload boundaries (ELF body claimed as JPEG,
                   oversize vs max_image_size_mb, zero-byte, NUL
                   filename, path-traversal, SVG-with-script);
                   JWT alg:none, signature/payload tamper, expired
                   session, PIN brute-force (serial + parallel),
                   admin password brute-force; deep authz (cross-user
                   delete, banned user across like/comment/feed-read,
                   host→admin escalation); small-scale DDoS (20× /join,
                   10MB comment body, 10 concurrent SSE).
  08-browser-chaos/ localStorage / sessionStorage / cookie purge,
                    IndexedDB drop mid-session, offline → reconnect,
                    slow-3G, 503 flakes, 429 with no retry storm,
                    multi-tab same/different user, no-JS, hostile CSS,
                    clock skew ±1h / -2d, localStorage quota exhausted.

Phase 3 — mobile gestures (runs only on chromium-mobile / Pixel 7):
  09-mobile/    touch-target ≥44px audit, env(safe-area-inset-bottom)
                structural check, long-press (FeedListCard → ContextSheet,
                quick-tap negation, click-suppression), double-tap
                (feed card like + lightbox heart-burst, via synthetic
                pointer events to bypass the first-tap-fires-click trap),
                viewport reflow (portrait/landscape/narrow/phablet),
                plus fixme stubs documenting planned gestures (swipe
                lightbox L/R, swipe-down dismiss, pull-to-refresh,
                long-press-comment).

Cross-UA matrix (chromium-engine projects run @smoke only):
  chromium-pixel7, chromium-galaxy-s22, samsung-internet (Samsung UA
  emulation on Galaxy viewport), edge-android, plus webkit-iphone,
  chrome-ios, firefox-android, firefox-desktop — the latter four need
  libavif16 on the host (Playwright dep) but the configs are in place.

Infrastructure:
  - fixtures/test.ts central test.extend (api, db, adminToken, guest,
    host, signIn). Per-test DB truncate via the dev-only POST
    /admin/__truncate route, gated by EVENTSNAP_TEST_MODE=1.
  - helpers/sse-listener.ts, helpers/upload-client.ts (Node-side
    multipart for adversarial file-upload tests + JPEG/PNG/ELF magic
    constants), helpers/touch.ts (longPress / doubleTap / swipe /
    inlineStyle / computedStyle).
  - 10 page objects covering every route + UploadSheet/Lightbox.
  - global-setup waits for /health, logs in admin, disables every
    rate-limit and quota toggle.
  - .github/workflows/e2e.yml: PR check runs chromium-desktop + the
    smoke matrix in parallel, uploads playwright-report/ and traces on
    failure.

Findings the suite surfaces as live `[finding]` warnings (not silenced):
  1. /admin/login has no rate-limit or lockout (bcrypt cost only).
  2. PIN-attempt counter races under parallel /recover requests.
  3. Zero-byte uploads pass /api/v1/upload.
  4. SVG-with-script can pass the magic-byte check (consider CSP +
     X-Content-Type-Options on /media/*).

Stack-internal docs live in e2e/README.md (UA tier table, Samsung
Internet escalation tiers A/B/C, debugging tips, roadmap).

Final tally: 134 passed / 0 failed / 9 skipped (test.fixme stubs for
not-yet-shipped gestures and one UI-upload-flow investigation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 19:37:11 +02:00

234 lines
8.7 KiB
Markdown

# EventSnap
> A private, QR-code-accessed photo & video sharing platform for weddings, birthdays, and personal events — built for guests, run by you.
---
## What is EventSnap?
At private events, photos and videos are scattered across dozens of guests' phones and never truly shared. Existing solutions (WhatsApp groups, Google Photos) require accounts, expose personal data, and lack event-specific social features.
**EventSnap** gives every guest instant, frictionless access to a shared, living gallery — **no app store, no email, no password.**
A guest scans the QR code on their way in, types their name, and is immediately part of a shared moment. They upload, react, and comment throughout the day. After the event, the host releases the gallery — every guest walks away with a beautiful offline HTML keepsake and the full archive.
**Project type:** Mobile-first PWA — runs in any browser, no installation required.
**Scale:** Personal / private use — one event at a time, ~100 guests, ~1,000 files.
---
## Features
### MVP
| Area | Feature |
|------|---------|
| **Onboarding** | QR code join flow, name-only registration, persistent JWT + recovery PIN, 30-day sessions |
| **Uploads** | Photo & video from library or live camera, client-side IndexedDB queue, per-file progress & retry, captions + #hashtags |
| **Processing** | Lossless server-side compression, feed preview generation, ffmpeg video thumbnails |
| **Feed** | Chronological grid, real-time SSE updates, hashtag filtering, likes & comments |
| **Host Dashboard** | Ban/unban guests, delete content, promote to host, lock event, release gallery for export |
| **Admin Dashboard** | All host permissions + configure limits, rates, quota tolerance, disk usage widget |
| **Export** | On-demand ZIP (full-quality originals) + self-contained offline `Memories.html` viewer |
### Planned (v1.x)
- Individual file download button
- Low-disk alert (< 10 GB free)
- Event banner / cover image
- Chunked resumable upload for large videos
- Host-curated story highlights
- Slideshow / presentation mode
---
## Tech Stack
| Layer | Technology |
|-------|-----------|
| Frontend | SvelteKit + TypeScript |
| Styling | Tailwind CSS v4 |
| Backend | Rust + Axum |
| Async | Tokio |
| Database | PostgreSQL 16 via SQLx (compile-time query checking) |
| Auth | Custom JWT (`jsonwebtoken`) + bcrypt PINs |
| Image processing | `image` crate + `oxipng` (lossless compression) |
| Video processing | ffmpeg via `tokio::process::Command` |
| File storage | Local disk (`/media/`) |
| Real-time | Axum SSE + `tokio::sync::broadcast` |
| Export | `async-zip` (streaming ZIP) + `minijinja` (HTML bundle) |
| Rate limiting | `tower-governor` (token-bucket, DB-configurable) |
| Reverse proxy | Caddy 2 (automatic HTTPS via Let's Encrypt) |
| Containers | Docker + Docker Compose |
| Infrastructure | Hetzner CX33 (4 vCPU, 8 GB RAM, 80 GB SSD) |
---
## Repository Structure
```
eventsnap/
├── backend/ # Rust + Axum API server
│ ├── src/
│ ├── Cargo.toml
│ └── Dockerfile
├── frontend/ # SvelteKit PWA
│ ├── src/
│ ├── svelte.config.js
│ └── Dockerfile
├── docker-compose.yml
├── Caddyfile
└── .env.example
```
---
## Getting Started
### Prerequisites
- [Docker](https://docs.docker.com/engine/install/) (includes Compose plugin)
- A domain name with an A record pointing to your server
### Deploy on a fresh VPS
```bash
# 1. Clone the repository
git clone https://git.mc02.dev/fabi/EventSnap.git
cd EventSnap
# 2. Configure environment
cp .env.example .env
nano .env # set DOMAIN, JWT_SECRET, ADMIN_PASSWORD_HASH, EVENT_NAME, etc.
# 3. Start the stack
docker compose up -d
```
Caddy automatically obtains a Let's Encrypt certificate on first start. The app is live at `https://DOMAIN` within ~30 seconds.
### Generate required secrets
```bash
# JWT secret (64 random bytes)
openssl rand -hex 64
# Admin password hash (bcrypt, cost 12)
htpasswd -bnBC 12 "" yourpassword | tr -d ':\n'
```
### Environment Variables
See [.env.example](.env.example) for the full list with descriptions and defaults. Key variables:
| Variable | Description |
|----------|-------------|
| `DOMAIN` | Public domain for TLS (e.g. `my-wedding.example.com`) |
| `JWT_SECRET` | 64-byte random hex string for signing JWTs |
| `ADMIN_PASSWORD_HASH` | bcrypt hash of the admin dashboard password |
| `EVENT_NAME` | Display name shown to guests |
| `EVENT_SLUG` | URL-safe event identifier |
| `DATABASE_URL` | PostgreSQL connection string |
---
## Docker Compose Stack
```
┌─────────────────────────────────────┐
│ Caddy :80 / :443 (TLS termination) │
└────────────┬────────────────────────┘
┌────────┴────────┐
│ │
┌───▼────┐ ┌─────▼──────┐
│ app │ │ frontend │
│ :3000 │ │ :3001 │
│ (Rust) │ │(SvelteKit) │
└───┬────┘ └────────────┘
┌───▼────┐
│ db │
│ :5432 │
│(Postgres)│
└────────┘
```
- `/api/*` and `/media/*` → Rust backend
- Everything else → SvelteKit frontend (`adapter-node`)
- Named volumes: `postgres_data`, `media_data`, `caddy_data`
---
## Backup
```bash
# Database snapshot
pg_dump $DATABASE_URL | gzip > /media/backups/db_$(date +%Y-%m-%d).sql.gz
# Weekly offsite sync (Hetzner Storage Box or similar)
rsync -az /opt/eventsnap/media/ user@storagebox.example.com:backup/eventsnap/
```
The `/media` volume holds originals, previews, thumbnails, exports, and DB backups — a single path to back up.
---
## Running the E2E test suite
Playwright-based end-to-end tests live in [`e2e/`](e2e/). They spin up an isolated docker-compose stack (Postgres on `:55432`, Caddy on `:3101`) and exercise the SvelteKit frontend against the real Rust backend with rate limits disabled.
```bash
cd e2e
npm install
npm run install:browsers # one-time
npm run stack:up # bring up the test stack
npm run test:e2e # full Phase 1 suite on chromium-desktop
npm run test:e2e:smoke # cross-UA matrix (chromium, samsung-internet, webkit, firefox, …)
npm run stack:down # tear it down
```
See [`e2e/README.md`](e2e/README.md) for the full UA matrix, Samsung Internet escalation tiers, and the Phase 2/3 roadmap.
CI runs this on every PR — see [`.github/workflows/e2e.yml`](.github/workflows/e2e.yml).
---
## Development Roadmap
Done:
- [x] Project blueprint & architecture
- [x] Monorepo scaffold (`backend/`, `frontend/`, Docker Compose)
- [x] DB schema + SQLx migrations (8 migrations through compression status + case-insensitive unique names)
- [x] Auth flow (join, JWT, 4-digit PIN with bcrypt + 3-attempt/15-min lockout, admin login)
- [x] Upload pipeline (multipart → compression worker via `tokio::sync::Semaphore` → SSE broadcast)
- [x] Client upload queue (IndexedDB, progress, retry, rate-limit auto-resume)
- [x] Gallery feed (list + grid toggle, SSE live updates, hashtag chips, in-memory search + autocomplete)
- [x] Camera capture (`getUserMedia` with front/back toggle, photo + `MediaRecorder` video)
- [x] Host Dashboard (event lock, gallery release, ban modal with hide-uploads choice, promote/demote, user search)
- [x] Admin Dashboard with inner tabs (Stats, Config, Export, Nutzer)
- [x] Export engine: streaming ZIP + SvelteKit-static HTML viewer (see [docs/CONCEPT_HTML_VIEWER.md](docs/CONCEPT_HTML_VIEWER.md))
- [x] Custom rate limiter (per-endpoint, hot-reloadable from `config` table)
- [x] Mobile-first redesign (bottom nav + FAB, see [docs/CONCEPT_MOBILE_UI.md](docs/CONCEPT_MOBILE_UI.md))
Open:
- [ ] Dynamic per-user storage quota enforcement (formula in [PROJECT.md §12](PROJECT.md); only tracking exists today)
- [ ] Own-upload deletion UI in the lightbox (backend route exists)
- [ ] SSE delta-fetch on foreground reconnect (scaffolded in [sse.ts](frontend/src/lib/sse.ts), not wired)
- [ ] Live diashow / slideshow mode — see [docs/CONCEPT_DIASHOW.md](docs/CONCEPT_DIASHOW.md)
- [ ] Individual file download button per post
- [ ] Low-disk alert (< 10 GB free)
- [ ] Event banner / cover image
- [ ] Chunked resumable upload for files > 100 MB
- [ ] Shared Tailwind config between main app and export-viewer
- [ ] End-to-end test event (10+ real devices on cellular)
See [docs/FEATURES.md](docs/FEATURES.md) for the up-to-date capability matrix by role.
Speculative / v2+ ideas live in [docs/IDEAS.md](docs/IDEAS.md).
---
## License
Private project — all rights reserved.