feat: implement tenant OAuth flow and guest achievements

This commit is contained in:
2025-09-25 08:32:37 +02:00
parent ef6203c603
commit b22d91ed32
84 changed files with 5984 additions and 1399 deletions

View File

@@ -1,4 +1,4 @@
# 07a Guest PWA Routes & Components
# 07a — Guest PWA Routes & Components
This scaffold describes recommended routes, guards, directories, and components for the Guest PWA. It is framework-leaning (React Router v6 + Vite), but adaptable.
@@ -8,25 +8,26 @@ Routing Principles
- Prefer modal routes (photo detail) layered over the gallery.
Route Map (proposed)
- `/` Landing (QR/PIN input; deep-link handler)
- `/setup` Profile Setup (name/avatar; skippable)
- `/e/:slug` Home/Feed (default gallery view + info bar)
- `/e/:slug/tasks` Task Picker (random/emotion)
- `/e/:slug/tasks/:taskId` Task Detail (card)
- `/e/:slug/upload` Upload Picker (camera/library + tagging)
- `/e/:slug/queue` Upload Queue (progress/retry)
- `/e/:slug/gallery` Gallery index (alias of Home or dedicated page)
- `/e/:slug/photo/:photoId` Photo Lightbox (modal over gallery)
- `/e/:slug/achievements` Achievements (optional)
- `/e/:slug/slideshow` Slideshow (optional, read-only)
- `/settings` — Settings (language/theme/cache/legal)
- `/legal/:page` — Legal pages (imprint/privacy/terms)
- `*` — NotFound
- `/` — Landing (QR/PIN input; deep-link handler)
- `/setup` — Profile Setup (name/avatar; skippable)
- `/e/:slug` — Home/Feed (default gallery view + info bar)
- `/e/:slug/tasks` — Task Picker (random/emotion)
- `/e/:slug/tasks/:taskId` — Task Detail (card)
- `/e/:slug/upload` — Upload Picker (camera/library + tagging)
- `/e/:slug/queue` — Upload Queue (progress/retry)
- `/e/:slug/gallery` — Gallery index (alias of Home or dedicated page)
- `/e/:slug/photo/:photoId` — Photo Lightbox (modal over gallery)
- `/e/:slug/achievements` — Achievements (optional)
- `/e/:slug/slideshow` — Slideshow (optional, read-only)
- `/legal/:page` — Legal pages (imprint/privacy/terms)
- `*` — NotFound
Note: The settings experience is handled via the header sheet (no dedicated route; legal pages stay routable under /legal/:page).
Guards & Loaders
- `EventGuard` verifies event token in storage; attempts refresh; otherwise redirects to `/`.
- `PrefetchEvent` loads event metadata/theme on `:slug` routes.
- `OfflineFallback` surfaces offline banner and queues mutations.
- `EventGuard` — verifies event token in storage; attempts refresh; otherwise redirects to `/`.
- `PrefetchEvent` — loads event metadata/theme on `:slug` routes.
- `OfflineFallback` — surfaces offline banner and queues mutations.
Suggested Directory Structure
```
@@ -46,7 +47,7 @@ apps/guest-pwa/
PhotoLightbox.tsx // modal route
AchievementsPage.tsx
SlideshowPage.tsx
SettingsPage.tsx
SettingsSheet.tsx
LegalPage.tsx
NotFoundPage.tsx
components/
@@ -116,7 +117,7 @@ export const router = createBrowserRouter([
{ path: 'slideshow', element: <SlideshowPage /> },
],
},
{ path: '/settings', element: <SettingsPage /> },
// Settings sheet is rendered inside Header; no standalone route.
{ path: '/legal/:page', element: <LegalPage /> },
{ path: '*', element: <NotFoundPage /> },
]);
@@ -124,11 +125,13 @@ export const router = createBrowserRouter([
Component Checklist
- Layout
- `Header`, `InfoBar` (X Gäste online Y Aufgaben gelöst), `BottomNav`, `Toast`.
- `Header`, `InfoBar` (X Gäste online • Y Aufgaben gelöst), `BottomNav`, `Toast`.
- Entry
- `QRPinForm` (QR deep link or PIN fallback), `ProfileForm` (name/avatar).
- Home/Feed
- `CTAButtons` (Random Task, Emotion Picker, Quick Photo), `GalleryMasonry`, `FiltersBar`, `PhotoCard`.
- `HeroCard` (Willkommensgruess + Eventtitel) und `StatTiles` (online Gaeste, geloeste Aufgaben, letztes Upload).
- `CTAButtons` (Aufgabe ziehen, Direkt-Upload, Galerie) + `UploadQueueLink` fuer Warteschlange.
- `EmotionPickerGrid` und `GalleryPreview` als inhaltlicher Einstieg.
- Tasks
- `EmotionPickerGrid`, `TaskCard` (shows duration, group size, actions).
- Capture/Upload (photos only)
@@ -136,7 +139,7 @@ Component Checklist
- Photo View
- `PhotoLightbox` (modal), like/share controls, emotion tags.
- Settings & Legal
- `SettingsPage` sections, `LegalPage` renderer.
- `SettingsSheet` (Header-Overlay mit Namenseditor, eingebetteten Rechtsdokumenten, Cache-Leeren), `LegalPage` Renderer.
State & Data
- TanStack Query for server data (events, photos); optimistic updates for likes.

View File

@@ -1,4 +1,4 @@
# 07 Guest PWA
# 07 — Guest PWA
Goal
- Delight guests with a frictionless, installable photo experience that works offline, respects privacy, and requires no account.
@@ -7,13 +7,13 @@ Non-Goals (MVP)
- No comments or chat. No facial recognition. No public profiles. No videos.
Personas
- Guest (attendee) scans QR, uploads photos, browses and likes.
- Host (tenant) optionally shares event PIN with guests; moderates via Tenant Admin PWA.
- Guest (attendee) — scans QR, uploads photos, browses and likes.
- Host (tenant) — optionally shares event PIN with guests; moderates via Tenant Admin PWA.
Top Journeys
- Join: Scan QR Open event Accept terms Optional PIN Land on Gallery.
- Upload: Add photos Review Submit Background upload Success/Retry.
- Browse: Infinite gallery Filter (emotion/task) Open photo Like/Share Back.
- Join: Scan QR → Open event → Accept terms → Optional PIN → Land on Gallery.
- Upload: Add photos → Review → Submit → Background upload → Success/Retry.
- Browse: Infinite gallery → Filter (emotion/task) → Open photo → Like/Share → Back.
Core Features
- Event access
@@ -25,14 +25,14 @@ Core Features
- Capture & upload
- Choose from camera or library; limit file size; show remaining upload cap.
- Client-side resize to sane max (e.g., 2560px longest edge); EXIF stripped client-side if available.
- Assign optional emotion/task before submit; default to Uncategorized.
- Assign optional emotion/task before submit; default to “Uncategorized”.
- Gallery
- Masonry grid, lazy-load, pull-to-refresh; open photo lightbox with swipe.
- Like (heart) with optimistic UI; share system sheet (URL to CDN variant).
- Filters: emotion, featured, mine (local-only tag for items uploaded from this device).
- Safety & abuse controls
- Rate limits per device and IP; content-length checks; mime/type sniffing.
- Upload moderation state: pending approved/hidden; show local status.
- Upload moderation state: pending → approved/hidden; show local status.
- Privacy & legal
- First run shows legal links (imprint/privacy); consent for push if enabled.
- No PII stored; guest name is optional free text and not required by default.
@@ -44,7 +44,7 @@ Screens
- Upload Picker: camera/library, selection preview, emotion/task tagging.
- Upload Queue: items with progress, retry, remove; background sync toggle.
- Photo Lightbox: zoom, like, share; show emotion tags.
- Settings: language, theme (system), clear cache, legal pages.
- Settings Sheet: Gear-Icon im Header oeffnet eine Sheet-Ansicht mit editierbarem Gastnamen, eingebetteten Rechtsdokumenten (inkl. Zurueck-Navigation) und Cache-Leeren; Theme-Umschalter bleibt im Header.
Wireframes
- See wireframes file at docs/wireframes/guest-pwa.md for low-fidelity layouts and flows.
@@ -55,28 +55,27 @@ Core Pages (Pflichtseiten)
- UI: Single input (QR deep link preferred, fallback PIN field) and Join button.
- States: invalid/expired token, event closed, offline (allow PIN entry and queue attempt).
- Profil-Setup (Name/Avatar)
- Purpose: Optional personalization for likes/attribution.
- UI: Name text field, optional avatar picker; one-time before first entry to event.
- Behavior: Skippable; editable later in Settings.
- Purpose: Optional personalisation fuer Likes/Statistiken; Name wird lokal im Browser gespeichert.
- UI: Name-Feld mit Sofort-Validierung; Avatar folgt spaeter.
- Behavior: Nicht verpflichtend, aber empfohlen; Name kann jederzeit im Settings Sheet angepasst oder geloescht werden.
- Startseite (Home/Feed)
- Purpose: Central hub; show event title/subheadline and CTAs.
- Header: Event name + subheadline (e.g., “Dein Fotospiel zur Hochzeit”).
- Info bar: “X Gäste online • Y Aufgaben gelöst”.
- CTAs: „Aufgabe ziehen“ (random), „Wie fühlst du dich?“ (emotion picker), small link “Einfach ein Foto machen”.
- Content: Gallery/Feed with photos + likes.
- Purpose: Central hub; begruesst Gaeste mit ihrem hinterlegten Namen und fuehrt zu den wichtigsten Aktionen.
- Header: Eventtitel plus Live-Kennzahlen (online Gaeste, geloeste Aufgaben); hero-card zeigt "Hey {Name}!".
- Highlights: Drei CTA-Karten fuer Aufgabe ziehen, Direkt-Upload und Galerie sowie ein Button fuer die Upload-Warteschlange.
- Content: EmotionPicker und GalleryPreview bilden weiterhin den Einstieg in Spielstimmung und aktuelle Fotos.
- Aufgaben-Flow
- Aufgaben-Picker: Choose random task or emotion mood.
- Aufgaben-Detail (Karte): Task text, emoji tag, estimated duration, suggested group size; actions: take photo, new task (same mood), change mood.
- Foto-Interaktion
- Kamera/Upload: Capture or pick; progress + success message on completion; background sync.
- Galerie/Übersicht: Grid/Feed; filters: Neueste, Beliebt, Meine; Like hearts.
- Galerie/Übersicht: Grid/Feed; filters: Neueste, Beliebt, Meine; Like hearts.
- Foto-Detailansicht: Fullscreen; likes/reactions; linked task + (optional) uploader name.
- Motivation & Spiel
- Achievements/Erfolge: Badges (e.g., Erstes Foto, 5 Aufgaben, Beliebtestes Foto); personal progress.
- Optionale Ergänzungen
- Slideshow/Präsentationsmodus: Auto-rotating gallery for TV/Beamer with likes/task overlay.
- Onboarding: 12 “So funktioniert das Spiel hints the first time.
- Event-Abschluss: Danke fürs Mitmachen, summary stats, link/QR to online gallery.
- Optionale Ergänzungen
- Slideshow/Präsentationsmodus: Auto-rotating gallery for TV/Beamer with likes/task overlay.
- Onboarding: 1–2 “So funktioniert das Spiel” hints the first time.
- Event-Abschluss: “Danke fürs Mitmachen”, summary stats, link/QR to online gallery.
Technical Notes
- Installability: PWA manifest + service worker; prompt A2HS on supported browsers.
@@ -84,15 +83,15 @@ Technical Notes
- Background Sync: use Background Sync API when available; fallback to retry on app open.
- Accessibility: large tap targets, high contrast, keyboard support, reduced motion.
- i18n: default `de`, fallback `en`; all strings in locale files; RTL not in MVP.
- Media types: Photos only (no videos) decision locked for MVP and v1.
- Media types: Photos only (no videos) — decision locked for MVP and v1.
- Realtime model: periodic polling (no WebSockets). Home counters every 10s; gallery delta every 30s with exponential backoff when tab hidden or offline.
API Touchpoints
- GET `/api/v1/events/{slug}` public event metadata (when open) + theme.
- GET `/api/v1/events/{slug}/photos` paginated gallery (approved only).
- POST `/api/v1/events/{slug}/photos` signed upload initiation; returns URL + fields.
- POST (S3) direct upload to object storage; then backend finalize call.
- POST `/api/v1/photos/{id}/like` idempotent like with device token.
- GET `/api/v1/events/{slug}` — public event metadata (when open) + theme.
- GET `/api/v1/events/{slug}/photos` — paginated gallery (approved only).
- POST `/api/v1/events/{slug}/photos` — signed upload initiation; returns URL + fields.
- POST (S3) — direct upload to object storage; then backend finalize call.
- POST `/api/v1/photos/{id}/like` — idempotent like with device token.
Limits (MVP defaults)
- Max uploads per device per event: 50
@@ -100,10 +99,10 @@ Limits (MVP defaults)
- Max resolution: 2560px longest edge per photo
Edge Cases
- Token expired/invalid Show Event closed/invalid link; link to retry.
- No connectivity Queue actions; show badge; retry policy with backoff.
- Storage full Offer to clear cache or deselect files.
- Permission denied (camera/photos) Explain and offer system shortcut.
- Token expired/invalid → Show “Event closed/invalid link”; link to retry.
- No connectivity → Queue actions; show badge; retry policy with backoff.
- Storage full → Offer to clear cache or deselect files.
- Permission denied (camera/photos) → Explain and offer system shortcut.
Decisions
- Videos are not supported (capture/upload strictly photos).

View File

@@ -7,7 +7,7 @@ This document outlines the authentication requirements and implementation detail
## Authentication Flow
### 1. Authorization Request
- **Endpoint**: `POST /oauth/authorize`
- **Endpoint**: `GET /api/v1/oauth/authorize`
- **Method**: GET (redirect from frontend)
- **Parameters**:
- `client_id`: Fixed client ID for tenant-admin-app (`tenant-admin-app`)
@@ -21,7 +21,7 @@ This document outlines the authentication requirements and implementation detail
**Response**: Redirect to frontend with authorization code and state parameters.
### 2. Token Exchange
- **Endpoint**: `POST /oauth/token`
- **Endpoint**: `POST /api/v1/oauth/token`
- **Method**: POST
- **Content-Type**: `application/x-www-form-urlencoded`
- **Parameters**:
@@ -44,7 +44,7 @@ This document outlines the authentication requirements and implementation detail
```
### 3. Token Refresh
- **Endpoint**: `POST /oauth/token`
- **Endpoint**: `POST /api/v1/oauth/token`
- **Method**: POST
- **Content-Type**: `application/x-www-form-urlencoded`
- **Parameters**:
@@ -102,6 +102,19 @@ This document outlines the authentication requirements and implementation detail
### oauth_clients Table
```sql
CREATE TABLE oauth_clients (
id VARCHAR(255) PRIMARY KEY,
client_id VARCHAR(255) UNIQUE NOT NULL,
client_secret VARCHAR(255),
tenant_id BIGINT UNSIGNED NULL,
redirect_uris JSON NULL,
scopes JSON NULL,
is_active TINYINT(1) DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT oauth_clients_tenant_id_foreign FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE SET NULL
);
```sql
CREATE TABLE oauth_clients (
id VARCHAR(255) PRIMARY KEY,
client_id VARCHAR(255) UNIQUE NOT NULL,
@@ -133,6 +146,20 @@ CREATE TABLE oauth_codes (
### refresh_tokens Table
```sql
CREATE TABLE refresh_tokens (
id VARCHAR(255) PRIMARY KEY,
tenant_id VARCHAR(255) NOT NULL,
client_id VARCHAR(255),
token VARCHAR(255) UNIQUE NOT NULL,
access_token VARCHAR(255),
scope TEXT,
ip_address VARCHAR(45),
user_agent TEXT,
expires_at TIMESTAMP,
revoked_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```sql
CREATE TABLE refresh_tokens (
id VARCHAR(255) PRIMARY KEY,
tenant_id VARCHAR(255) NOT NULL,
@@ -261,18 +288,22 @@ VITE_OAUTH_CLIENT_ID=tenant-admin-app
- Revoke old refresh token immediately
- Limit refresh tokens per tenant to 5 active
### 3. Rate Limiting
### 3. Key Management
- RSA key pairs for signing are generated on demand and stored in storage/app/private.key (private) and storage/app/public.key (public).
- Treat the private key as a secret; rotate it alongside deploys that invalidate tenant tokens.
### 4. Rate Limiting
- Authorization requests: 10/minute per IP
- Token exchanges: 5/minute per IP
- Token validation: 100/minute per tenant
### 4. Logging and Monitoring
### 5. Logging and Monitoring
- Log all authentication attempts (success/failure)
- Monitor token usage patterns
- Alert on unusual activity (multiple failed attempts, token anomalies)
- Track refresh token usage for security analysis
### 5. Database Cleanup
### 6. Database Cleanup
- Cron job to remove expired authorization codes (daily)
- Remove expired refresh tokens (weekly)
- Clean blacklisted tokens after expiry (daily)
@@ -315,4 +346,10 @@ VITE_OAUTH_CLIENT_ID=tenant-admin-app
- Alert on PKCE validation failures
- Log all security-related events
This implementation provides secure, scalable authentication for the Fotospiel tenant system, following OAuth2 best practices with PKCE for public clients.
This implementation provides secure, scalable authentication for the Fotospiel tenant system, following OAuth2 best practices with PKCE for public clients.

View File

@@ -1,4 +1,4 @@
# API-Nutzung der Tenant Admin App
# API-Nutzung der Tenant Admin App
Diese Dokumentation beschreibt alle API-Endpunkte, die die Tenant Admin App mit der Backend-Hauptapp kommuniziert. Alle Requests sind tenant-scoped und erfordern JWT-Authentifizierung.
@@ -19,6 +19,7 @@ Diese Dokumentation beschreibt alle API-Endpunkte, die die Tenant Admin App mit
- **Response**: Neuer Access/Refresh-Token
- **Token Validation**: `GET /api/v1/tenant/me`
- **Redirect URI**: Standardmaessig `${origin}/admin/auth/callback` (per Vite-Env anpassbar)
- **Headers**: `Authorization: Bearer {access_token}`
- **Response**: `{ id, email, tenant_id, role, name }`
@@ -51,18 +52,18 @@ Diese Dokumentation beschreibt alle API-Endpunkte, die die Tenant Admin App mit
- **Validierung**: Prüft Credit-Balance (1 Credit pro Event)
### Event-Details
- **GET /api/v1/tenant/events/{id}**
- **GET /api/v1/tenant/events/{slug}**
- **Headers**: `Authorization: Bearer {token}`
- **Response**: Erweitertes Event mit `{ tasks[], members, stats { likes, views, uploads } }`
### Event updaten
- **PATCH /api/v1/tenant/events/{id}**
- **PATCH /api/v1/tenant/events/{slug}**
- **Headers**: `Authorization: Bearer {token}`, `Content-Type: application/json`, `If-Match: {etag}`
- **Body**: Partial Event-Daten (title, date, location, description)
- **Response**: Updated Event
### Event archivieren
- **DELETE /api/v1/tenant/events/{id}**
- **DELETE /api/v1/tenant/events/{slug}**
- **Headers**: `Authorization: Bearer {token}`, `If-Match: {etag}`
- **Response**: 204 No Content (soft-delete)
@@ -156,12 +157,26 @@ Diese Dokumentation beschreibt alle API-Endpunkte, die die Tenant Admin App mit
- **Response**: `{ balance: number }`
### Ledger-Verlauf
- **GET /api/v1/tenant/ledger**
- **GET /api/v1/tenant/credits/ledger**
- **Headers**: `Authorization: Bearer {token}`
- **Params**: `page`, `per_page` (Pagination)
- **Response**: `{ data: LedgerEntry[], current_page, last_page }`
- **LedgerEntry**: `{ id, type, amount, credits, date, description, transactionId? }`
### Credits kaufen (In-App)
- **POST /api/v1/tenant/credits/purchase**
- **Headers**: `Authorization: Bearer {token}`, `Content-Type: application/json`
- **Body**: `{ package_id: string, credits_added: number, platform?: 'capacitor'|'web', transaction_id?: string, subscription_active?: boolean }`
- **Response**: `{ message, balance, subscription_active }`
- **Hinweis**: Wird nach erfolgreichen In-App-Kuferfolgen aufgerufen, aktualisiert Balance & Ledger.
### Credits synchronisieren
- **POST /api/v1/tenant/credits/sync**
- **Headers**: `Authorization: Bearer {token}`, `Content-Type: application/json`
- **Body**: `{ balance: number, subscription_active: boolean, last_sync: ISODateString }`
- **Response**: `{ balance, subscription_active, server_time }`
- **Hinweis**: Client meldet lokalen Stand; Server gibt Quelle-der-Wahrheit zurcck.
### Kauf-Intent erstellen
- **POST /api/v1/tenant/purchases/intent**
- **Headers**: `Authorization: Bearer {token}`, `Content-Type: application/json`
@@ -242,8 +257,9 @@ curl -H "Authorization: Bearer {token}" \
## Deployment
### Environment-Variablen
- **REACT_APP_API_URL**: Backend-API-URL (Pflicht)
- **REACT_APP_OAUTH_CLIENT_ID**: OAuth-Client-ID (Pflicht)
- **VITE_API_URL**: Backend-API-URL (Pflicht)
- **VITE_OAUTH_CLIENT_ID**: OAuth-Client-ID (Pflicht)
- **VITE_REVENUECAT_PUBLIC_KEY**: Optional fuer In-App-Kaeufe (RevenueCat)
### Build & Deploy
1. **Development**: `npm run dev`
@@ -254,4 +270,5 @@ curl -H "Authorization: Bearer {token}" \
- App ist PWA-fähig (Manifest, Service Worker).
- Installierbar auf Desktop/Mobile via "Zum Startbildschirm hinzufügen".
Für weitere Details siehe die spezifischen Dokumentationsdateien.
Für weitere Details siehe die spezifischen Dokumentationsdateien.