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>
234 lines
8.7 KiB
Markdown
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.
|