Backend: - SQLite → PostgreSQL (pg_trgm search, async services) - All services rewritten to async with pg Pool - Data imported (50 recipes, 8 categories) - better-sqlite3 removed Frontend: - ProfilePage complete (edit profile, change password, no more stubs) - HouseholdCard (create, join via code, manage members, leave) - Shopping scope toggle (personal/household) - IngredientPickerModal (smart add with basics filter) - Auth token auto-attached to all API calls (token.ts) - Removed PlaceholderPage Infrastructure: - Docker Compose (backend + frontend + postgres) - Dockerfile for backend (node:22-alpine + tsx) - Dockerfile for frontend (vite build + nginx) - nginx.conf with API proxy + SPA fallback - .env.example for production secrets Spec: - AUTH-V2-SPEC updated: household join flow, manual shopping items
891 lines
23 KiB
Markdown
891 lines
23 KiB
Markdown
# Luna Recipes — Auth v2 Feature-Spezifikation
|
||
|
||
## Übersicht
|
||
|
||
Implementierung eines vollständigen Authentifizierungssystems für die Luna Rezept-App mit Multi-User-Support und Haushaltsfunktionalität. Das System ermöglicht es mehreren Nutzern, die App zu verwenden, dabei ihre Daten zu trennen und optional einen gemeinsamen Haushalt zu teilen.
|
||
|
||
## 1. Datenmodell
|
||
|
||
### 1.1 Neue Tabellen
|
||
|
||
#### users
|
||
```sql
|
||
CREATE TABLE users (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
email TEXT UNIQUE NOT NULL,
|
||
password_hash TEXT NOT NULL,
|
||
display_name TEXT NOT NULL,
|
||
avatar_url TEXT,
|
||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||
);
|
||
|
||
CREATE UNIQUE INDEX idx_users_email ON users(email);
|
||
```
|
||
|
||
#### households
|
||
```sql
|
||
CREATE TABLE households (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
name TEXT NOT NULL,
|
||
invite_code TEXT UNIQUE NOT NULL,
|
||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||
);
|
||
|
||
CREATE UNIQUE INDEX idx_households_invite_code ON households(invite_code);
|
||
```
|
||
|
||
#### household_members
|
||
```sql
|
||
CREATE TABLE household_members (
|
||
household_id UUID REFERENCES households(id) ON DELETE CASCADE,
|
||
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||
role TEXT NOT NULL CHECK (role IN ('owner', 'member')),
|
||
joined_at TIMESTAMPTZ DEFAULT NOW(),
|
||
PRIMARY KEY (household_id, user_id)
|
||
);
|
||
```
|
||
|
||
#### user_favorites (Neue Tabelle)
|
||
```sql
|
||
CREATE TABLE user_favorites (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||
recipe_id UUID REFERENCES recipes(id) ON DELETE CASCADE,
|
||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||
UNIQUE(user_id, recipe_id)
|
||
);
|
||
```
|
||
|
||
### 1.2 Erweiterte bestehende Tabellen
|
||
|
||
#### shopping_items
|
||
```sql
|
||
-- Erweitern um User- und Haushaltszuordnung
|
||
ALTER TABLE shopping_items ADD COLUMN user_id UUID REFERENCES users(id);
|
||
ALTER TABLE shopping_items ADD COLUMN household_id UUID REFERENCES households(id);
|
||
|
||
-- Migration: NULL user_id wird beim ersten Login zugewiesen
|
||
```
|
||
|
||
#### notes
|
||
```sql
|
||
-- Erweitern um User-Zuordnung
|
||
ALTER TABLE notes ADD COLUMN user_id UUID REFERENCES users(id);
|
||
|
||
-- Migration: Bestehende Notizen werden dem ersten registrierten User zugewiesen
|
||
```
|
||
|
||
#### recipes
|
||
```sql
|
||
-- Recipes bleiben global, aber mit Ersteller-Info
|
||
ALTER TABLE recipes ADD COLUMN created_by UUID REFERENCES users(id);
|
||
|
||
-- Migration: Bestehende Rezepte werden dem ersten registrierten User zugewiesen
|
||
```
|
||
|
||
### 1.3 Daten-Ownership
|
||
|
||
- **Recipes:** Global sichtbar, `created_by` zur Information
|
||
- **Shopping Items:** Pro User ODER pro Haushalt (wenn `household_id` gesetzt)
|
||
- **Favorites:** Pro User (user_favorites Tabelle)
|
||
- **Notes:** Pro User
|
||
- **Households:** Gemeinsam für alle Mitglieder
|
||
|
||
## 2. API Endpoints
|
||
|
||
### 2.1 Authentication
|
||
|
||
#### POST /api/auth/register
|
||
```json
|
||
// Request
|
||
{
|
||
"email": "luna@example.com",
|
||
"password": "secure123!",
|
||
"display_name": "Luna"
|
||
}
|
||
|
||
// Response (201 Created)
|
||
{
|
||
"success": true,
|
||
"user": {
|
||
"id": "uuid-here",
|
||
"email": "luna@example.com",
|
||
"display_name": "Luna",
|
||
"avatar_url": null,
|
||
"created_at": "2026-02-18T15:30:00Z"
|
||
},
|
||
"access_token": "jwt-token-here"
|
||
}
|
||
|
||
// Error (400 Bad Request)
|
||
{
|
||
"success": false,
|
||
"error": "EMAIL_EXISTS",
|
||
"message": "Ein Account mit dieser E-Mail existiert bereits"
|
||
}
|
||
```
|
||
|
||
#### POST /api/auth/login
|
||
```json
|
||
// Request
|
||
{
|
||
"email": "luna@example.com",
|
||
"password": "secure123!"
|
||
}
|
||
|
||
// Response (200 OK)
|
||
{
|
||
"success": true,
|
||
"user": {
|
||
"id": "uuid-here",
|
||
"email": "luna@example.com",
|
||
"display_name": "Luna",
|
||
"avatar_url": null
|
||
},
|
||
"access_token": "jwt-token-here"
|
||
}
|
||
|
||
// Error (401 Unauthorized)
|
||
{
|
||
"success": false,
|
||
"error": "INVALID_CREDENTIALS",
|
||
"message": "E-Mail oder Passwort ungültig"
|
||
}
|
||
```
|
||
|
||
#### POST /api/auth/logout
|
||
```json
|
||
// Response (200 OK)
|
||
{
|
||
"success": true,
|
||
"message": "Erfolgreich abgemeldet"
|
||
}
|
||
```
|
||
|
||
#### GET /api/auth/me
|
||
```json
|
||
// Response (200 OK)
|
||
{
|
||
"success": true,
|
||
"user": {
|
||
"id": "uuid-here",
|
||
"email": "luna@example.com",
|
||
"display_name": "Luna",
|
||
"avatar_url": "https://example.com/avatar.jpg",
|
||
"household": {
|
||
"id": "household-uuid",
|
||
"name": "Luna & Marc",
|
||
"role": "owner"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### PUT /api/auth/me
|
||
```json
|
||
// Request (Profil bearbeiten)
|
||
{
|
||
"display_name": "Luna Schmidt",
|
||
"avatar_url": "https://example.com/new-avatar.jpg"
|
||
}
|
||
|
||
// Response (200 OK)
|
||
{
|
||
"success": true,
|
||
"user": {
|
||
"id": "uuid-here",
|
||
"email": "luna@example.com",
|
||
"display_name": "Luna Schmidt",
|
||
"avatar_url": "https://example.com/new-avatar.jpg"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### PUT /api/auth/me/password
|
||
```json
|
||
// Request
|
||
{
|
||
"current_password": "old123!",
|
||
"new_password": "new456!"
|
||
}
|
||
|
||
// Response (200 OK)
|
||
{
|
||
"success": true,
|
||
"message": "Passwort erfolgreich geändert"
|
||
}
|
||
|
||
// Error (400 Bad Request)
|
||
{
|
||
"success": false,
|
||
"error": "INVALID_CURRENT_PASSWORD",
|
||
"message": "Aktuelles Passwort ist falsch"
|
||
}
|
||
```
|
||
|
||
### 2.2 Households
|
||
|
||
#### POST /api/households
|
||
```json
|
||
// Request (Haushalt erstellen)
|
||
{
|
||
"name": "Luna & Marc"
|
||
}
|
||
|
||
// Response (201 Created)
|
||
{
|
||
"success": true,
|
||
"household": {
|
||
"id": "household-uuid",
|
||
"name": "Luna & Marc",
|
||
"invite_code": "COOK2024",
|
||
"created_at": "2026-02-18T15:30:00Z",
|
||
"members": [
|
||
{
|
||
"user_id": "user-uuid",
|
||
"display_name": "Luna",
|
||
"role": "owner",
|
||
"joined_at": "2026-02-18T15:30:00Z"
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
#### POST /api/households/:id/invite
|
||
```json
|
||
// Response (200 OK) - Neuen Einladungscode generieren
|
||
{
|
||
"success": true,
|
||
"invite_code": "COOK2025",
|
||
"expires_at": "2026-02-25T15:30:00Z"
|
||
}
|
||
```
|
||
|
||
#### POST /api/households/join
|
||
```json
|
||
// Request
|
||
{
|
||
"invite_code": "COOK2024"
|
||
}
|
||
|
||
// Response (200 OK)
|
||
{
|
||
"success": true,
|
||
"household": {
|
||
"id": "household-uuid",
|
||
"name": "Luna & Marc",
|
||
"role": "member"
|
||
}
|
||
}
|
||
|
||
// Error (400 Bad Request)
|
||
{
|
||
"success": false,
|
||
"error": "INVALID_INVITE_CODE",
|
||
"message": "Einladungscode ungültig oder abgelaufen"
|
||
}
|
||
```
|
||
|
||
#### GET /api/households/mine
|
||
```json
|
||
// Response (200 OK)
|
||
{
|
||
"success": true,
|
||
"household": {
|
||
"id": "household-uuid",
|
||
"name": "Luna & Marc",
|
||
"invite_code": "COOK2024",
|
||
"role": "owner",
|
||
"members": [
|
||
{
|
||
"user_id": "user1-uuid",
|
||
"display_name": "Luna",
|
||
"role": "owner",
|
||
"joined_at": "2026-02-18T15:30:00Z"
|
||
},
|
||
{
|
||
"user_id": "user2-uuid",
|
||
"display_name": "Marc",
|
||
"role": "member",
|
||
"joined_at": "2026-02-19T10:15:00Z"
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
## 3. Frontend Pages
|
||
|
||
### 3.1 Authentication Pages
|
||
|
||
#### /login
|
||
- **Design:** Vollbild-Login mit Luna Recipes Logo
|
||
- **Felder:** E-Mail, Passwort, "Angemeldet bleiben" Checkbox
|
||
- **Actions:** Login, "Registrieren" Link, "Passwort vergessen" Link (v2.1)
|
||
- **Mobile:** Touch-optimierte Input-Felder, große Login-Button
|
||
- **Validation:** Client-side Validation mit sofortigem Feedback
|
||
- **States:** Loading State beim Login-Prozess
|
||
|
||
#### /register
|
||
- **Design:** Gleiche Basis wie Login-Page
|
||
- **Felder:** E-Mail, Passwort, Passwort wiederholen, Display Name
|
||
- **Validation:**
|
||
- E-Mail Format-Check
|
||
- Passwort-Stärke Indikator
|
||
- Passwort-Match Validation
|
||
- Display Name min. 2 Zeichen
|
||
- **Actions:** Registrieren, "Schon Account?" Login-Link
|
||
- **Flow:** Nach erfolgreicher Registrierung → automatisch eingeloggt → Dashboard
|
||
|
||
### 3.2 Profile Pages
|
||
|
||
#### /profile
|
||
- **Layout:** Card-basiert, vertikal gestapelt (kein Tab-Interface)
|
||
- **Profil-Karte:**
|
||
- Avatar (Upload oder URL)
|
||
- Display Name (inline editierbar)
|
||
- E-Mail (nicht editierbar, mit "Ändern" Link für v2.1)
|
||
- "Passwort ändern" Button
|
||
- **Haushalt-Karte:** (immer sichtbar, direkt unter Profil)
|
||
- **Kein Haushalt:** Card mit 🏠 Icon, "Haushalt erstellen" + "Beitreten" Buttons
|
||
- **In Haushalt:** Haushalt-Name, Mitglieder-Avatare, "Verwalten" Button → `/profile/household`
|
||
- Visuell prominent — nicht versteckt in einem Tab!
|
||
- **Quick-Actions:** Logout-Button unten
|
||
- **Mobile:** Große Avatar-Anzeige, Touch-freundliche Edit-Buttons, 44px Targets
|
||
|
||
#### /profile/edit
|
||
- **Modal oder Fullscreen (Mobile):** Profil bearbeiten
|
||
- **Felder:** Display Name, Avatar URL/Upload
|
||
- **Actions:** Speichern, Abbrechen
|
||
- **Validation:** Display Name required
|
||
|
||
#### /profile/password
|
||
- **Layout:** Focused Passwort-Ändern Page
|
||
- **Felder:** Aktuelles Passwort, Neues Passwort, Bestätigung
|
||
- **Security:**
|
||
- Passwort-Stärke Anzeige
|
||
- "Passwort anzeigen" Toggle
|
||
- Session-Refresh nach Änderung
|
||
- **UX:** Erfolgs-Toast + Redirect nach Speichern
|
||
|
||
#### /profile/household
|
||
- **States:**
|
||
- **Kein Haushalt:** "Haushalt erstellen" oder "Haushalt beitreten"
|
||
- **Haushalt Owner:** Mitglieder-Liste, Einladungscode verwalten
|
||
- **Haushalt Member:** Mitglieder-Liste, "Haushalt verlassen"
|
||
- **Features:**
|
||
- Einladungscode kopieren/teilen
|
||
- QR-Code für Einladung (Mobile-optimiert)
|
||
- Mitglieder-Management (nur Owner)
|
||
|
||
## 4. Authentication Flow
|
||
|
||
### 4.1 Token-Strategy
|
||
|
||
#### JWT Access Token
|
||
- **Laufzeit:** 15 Minuten
|
||
- **Storage:** Memory (React State + Context)
|
||
- **Payload:**
|
||
```json
|
||
{
|
||
"sub": "user-uuid",
|
||
"email": "luna@example.com",
|
||
"display_name": "Luna",
|
||
"household_id": "household-uuid",
|
||
"iat": 1708272600,
|
||
"exp": 1708273500
|
||
}
|
||
```
|
||
|
||
#### Refresh Token
|
||
- **Laufzeit:** 30 Tage
|
||
- **Storage:** httpOnly Cookie, Secure, SameSite=Strict
|
||
- **Rotation:** Neuer Refresh Token bei jedem Access Token Refresh
|
||
- **Cookie Name:** `luna_refresh_token`
|
||
|
||
### 4.2 Auto-Refresh Flow
|
||
|
||
```typescript
|
||
// Auth Context implementiert Auto-Refresh
|
||
const authContext = {
|
||
// Access Token im Memory
|
||
accessToken: string | null,
|
||
// User Info aus Token dekodiert
|
||
user: User | null,
|
||
// Auto-refresh 2 Minuten vor Ablauf
|
||
refreshToken: () => Promise<void>,
|
||
logout: () => void
|
||
}
|
||
|
||
// Axios Interceptor für Auto-Refresh
|
||
axios.interceptors.response.use(
|
||
(response) => response,
|
||
async (error) => {
|
||
if (error.response?.status === 401) {
|
||
await authContext.refreshToken()
|
||
// Retry original request
|
||
return axios.request(error.config)
|
||
}
|
||
return Promise.reject(error)
|
||
}
|
||
)
|
||
```
|
||
|
||
### 4.3 Route Protection
|
||
|
||
```typescript
|
||
// Protected Route Component
|
||
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||
const { user, loading } = useAuth()
|
||
|
||
if (loading) return <LoadingSpinner />
|
||
if (!user) return <Navigate to="/login" replace />
|
||
|
||
return <>{children}</>
|
||
}
|
||
|
||
// App Router
|
||
<Routes>
|
||
<Route path="/login" element={<LoginPage />} />
|
||
<Route path="/register" element={<RegisterPage />} />
|
||
<Route path="/" element={
|
||
<ProtectedRoute>
|
||
<Dashboard />
|
||
</ProtectedRoute>
|
||
} />
|
||
</Routes>
|
||
```
|
||
|
||
### 4.4 Login Flow States
|
||
|
||
1. **Unauthenticated:** Redirect zu `/login`
|
||
2. **Login Success:**
|
||
- Set Access Token in Memory
|
||
- Set Refresh Token in Cookie
|
||
- Redirect zu ursprünglicher Route oder `/`
|
||
3. **Token Expired:** Auto-refresh, bei Fehler → Logout
|
||
4. **Logout:** Clear Token + Cookie, Redirect zu `/login`
|
||
|
||
## 5. Haushalt-Feature
|
||
|
||
### 5.1 Haushalt erstellen
|
||
|
||
1. User klickt "Haushalt erstellen" in `/profile/household`
|
||
2. Modal: Haushalt-Name eingeben
|
||
3. Backend erstellt Haushalt + generiert Einladungscode
|
||
4. User wird automatisch als Owner hinzugefügt
|
||
5. Einladungscode wird angezeigt zum Teilen
|
||
|
||
### 5.2 Haushalt beitreten
|
||
|
||
#### Flow
|
||
1. User erhält Einladungscode (per Link, QR-Code oder manuell)
|
||
2. In `/profile/household` → Button "Haushalt beitreten"
|
||
3. **Eingabe:** 6-stelliger Code (uppercase, alphanumerisch, z.B. `COOK2A`)
|
||
4. Backend validiert Code → fügt User als `member` hinzu
|
||
5. Erfolg: Konfetti 🎉 + Toast "Willkommen bei [Haushalt-Name]!"
|
||
6. Einkaufsliste zeigt ab sofort Haushalt-Items
|
||
|
||
#### Deep Link Support
|
||
- URL-Format: `https://luna.supertoll.xyz/join/{invite_code}`
|
||
- Wenn eingeloggt → direkt beitreten (mit Bestätigungs-Dialog)
|
||
- Wenn nicht eingeloggt → Redirect zu `/login?redirect=/join/{code}`
|
||
- Nach Login → automatisch Join-Flow fortsetzen
|
||
|
||
#### QR-Code
|
||
- Owner kann QR-Code generieren (enthält Deep Link)
|
||
- Share-Button: QR als Bild teilen oder Code kopieren
|
||
- Library: `qrcode.react` (lightweight)
|
||
|
||
#### Fehlerbehandlung
|
||
- `INVALID_INVITE_CODE` → "Code ungültig oder abgelaufen"
|
||
- `ALREADY_IN_HOUSEHOLD` → "Du bist bereits in einem Haushalt. Zuerst verlassen?"
|
||
- `HOUSEHOLD_FULL` → "Haushalt hat maximale Mitgliederzahl erreicht" (Limit: 10)
|
||
- Netzwerkfehler → Retry-Button
|
||
|
||
#### Haushalt verlassen
|
||
- Member können jederzeit verlassen (Bestätigungs-Dialog)
|
||
- Owner kann nur verlassen wenn ein anderer Member zum Owner gemacht wird
|
||
- Letzter Member → Haushalt wird gelöscht
|
||
- Nach Verlassen: eigene Shopping-Items bleiben privat erhalten
|
||
|
||
### 5.3 Gemeinsame Einkaufsliste
|
||
|
||
#### Datenlogik
|
||
- Shopping Items mit `household_id` sind für alle Haushaltsmitglieder sichtbar
|
||
- Shopping Items mit nur `user_id` sind privat
|
||
- Standard: Neue Items werden Haushalt zugeordnet (wenn User in Haushalt ist)
|
||
- Toggle: "Privat" Checkbox beim Hinzufügen
|
||
|
||
#### UI/UX
|
||
- **Indicator:** Haushalt-Items haben kleines Haushalt-Icon
|
||
- **Filter:** "Alle", "Haushalt", "Privat" Tabs in Shopping Liste
|
||
- **Mobile:** Swipe-Actions: "Als privat markieren" / "Mit Haushalt teilen"
|
||
|
||
### 5.4 Smarte Mengenabfrage beim Rezept-Einkauf
|
||
|
||
Aktuell werden beim "Zur Einkaufsliste hinzufügen" alle Zutaten 1:1 übernommen. Aber oft hat man schon Mehl, Eier, Zucker etc. daheim. Statt blind alles draufzupacken → User fragen was noch fehlt.
|
||
|
||
#### Flow: "Was brauchst du noch?"
|
||
1. User klickt "Zutaten zur Einkaufsliste" auf der Rezeptseite
|
||
2. **Modal/Sheet öffnet sich** mit allen Zutaten als Checkliste
|
||
3. Jede Zutat hat:
|
||
- ☑️ Checkbox (Standard: alle an)
|
||
- Name + Menge aus Rezept
|
||
- Optional: Menge anpassen (z.B. "hab noch 200g, brauch nur 300g")
|
||
4. User deaktiviert was schon da ist
|
||
5. Button: "X Artikel hinzufügen" (zeigt Anzahl der aktiven)
|
||
6. Nur ausgewählte Zutaten landen auf der Liste
|
||
|
||
#### Quick-Actions im Modal
|
||
- **"Alles"** — alle Checkboxen an (default)
|
||
- **"Basics abwählen"** — typische Vorrats-Zutaten automatisch deaktivieren (Salz, Pfeffer, Öl, Wasser)
|
||
- **"Nichts"** — alle aus, manuell auswählen
|
||
|
||
#### Basics-Liste (konfigurierbar pro User/Haushalt)
|
||
Standard-Basics die man meistens daheim hat:
|
||
- Salz, Pfeffer, Zucker, Mehl, Öl, Wasser, Butter, Eier
|
||
- User kann in Settings eigene Basics definieren
|
||
- Passt zu Luna: Mehl ✅ Zucker ✅ Eier ✅ Backpulver ✅ (Butter ❌ muss immer drauf 😄)
|
||
|
||
#### Späterer Ausbau (v2+: Vorratskammer/Pantry)
|
||
- Automatischer Abgleich mit Vorratskammer
|
||
- "Hab ich schon" wird automatisch vorausgewählt
|
||
- Fehlmengen werden berechnet (Rezept braucht 500g, hast 200g → 300g auf Liste)
|
||
|
||
### 5.5 Manuelle Artikel zur Einkaufsliste hinzufügen
|
||
|
||
Aktuell kommen Shopping-Items nur aus Rezepten. User sollen auch eigene Artikel hinzufügen können (z.B. "Klopapier", "Spülmittel", Zutaten ohne Rezept).
|
||
|
||
#### API
|
||
|
||
##### POST /api/shopping/manual
|
||
```json
|
||
// Request
|
||
{
|
||
"name": "Klopapier",
|
||
"amount": "1",
|
||
"unit": "Packung",
|
||
"private": false
|
||
}
|
||
|
||
// Response (201 Created)
|
||
{
|
||
"success": true,
|
||
"item": {
|
||
"id": "uuid",
|
||
"name": "Klopapier",
|
||
"amount": "1",
|
||
"unit": "Packung",
|
||
"checked": false,
|
||
"recipe_id": null,
|
||
"user_id": "user-uuid",
|
||
"household_id": "household-uuid",
|
||
"created_at": "2026-02-18T16:00:00Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### DB-Erweiterung
|
||
```sql
|
||
-- recipe_id wird NULLABLE (ist es vermutlich schon)
|
||
-- Manuelle Items haben recipe_id = NULL
|
||
-- Optional: source-Feld um Herkunft zu unterscheiden
|
||
ALTER TABLE shopping_items ADD COLUMN source TEXT DEFAULT 'recipe'
|
||
CHECK (source IN ('recipe', 'manual'));
|
||
```
|
||
|
||
#### Frontend UI
|
||
|
||
##### Eingabefeld (oben in der Einkaufsliste)
|
||
- **Sticky Input-Bar** oben auf der Shopping-Page
|
||
- Textfeld mit Placeholder "Artikel hinzufügen..." + ➕ Button
|
||
- **Smart Parsing:** "2 Liter Milch" → amount: "2", unit: "Liter", name: "Milch"
|
||
- **Autocomplete:** Vorschläge aus bisherigen Items (häufig gekaufte)
|
||
- Enter oder ➕ fügt hinzu, Feld wird geleert
|
||
- Standard: Haushalt-Item (wenn in Haushalt), sonst privat
|
||
|
||
##### Smart Parsing Regeln
|
||
```
|
||
"Milch" → name: "Milch", amount: null, unit: null
|
||
"2 Milch" → name: "Milch", amount: "2", unit: null
|
||
"2 Liter Milch" → name: "Milch", amount: "2", unit: "Liter"
|
||
"500g Mehl" → name: "Mehl", amount: "500", unit: "g"
|
||
"1 Packung Butter" → name: "Butter", amount: "1", unit: "Packung"
|
||
```
|
||
|
||
##### Visuelle Unterscheidung
|
||
- Rezept-Items: normaler Style + Rezept-Name als Subtitle
|
||
- Manuelle Items: leicht anderer Style (z.B. 📝 Icon oder kursiver Subtitle "Manuell hinzugefügt")
|
||
- Haushalt-Items: 🏠 Badge
|
||
- Private Items: 🔒 Badge
|
||
|
||
##### Quick-Add Vorschläge
|
||
- Unter dem Input: Chips mit häufig hinzugefügten Artikeln
|
||
- Max 5 Vorschläge, basierend auf History
|
||
- Tap = sofort hinzufügen
|
||
|
||
### 5.4 Persönliche Favoriten
|
||
|
||
- Favoriten sind immer pro User (`user_favorites` Tabelle)
|
||
- Keine Sharing-Option für Favoriten in v2
|
||
- Favoriten-Liste zeigt nur eigene Favoriten
|
||
|
||
## 6. Migration Strategy
|
||
|
||
### 6.1 Backwards Compatibility
|
||
|
||
- **Anonymous Access:** Rezepte bleiben öffentlich zugänglich ohne Login
|
||
- **URLs:** Alle bestehenden Recipe-URLs funktionieren weiterhin
|
||
- **Features:** Basis-Funktionen (Rezepte suchen, ansehen) ohne Auth
|
||
|
||
### 6.2 Daten-Migration
|
||
|
||
#### Phase 1: Schema Updates
|
||
```sql
|
||
-- Neue Tabellen erstellen (siehe Datenmodell)
|
||
-- Bestehende Tabellen erweitern (users_id Spalten als optional)
|
||
|
||
-- Default User erstellen für Migration
|
||
INSERT INTO users (id, email, display_name, password_hash)
|
||
VALUES (
|
||
'migration-user-uuid',
|
||
'migration@luna-recipes.local',
|
||
'Luna (Migration)',
|
||
'no-login'
|
||
);
|
||
```
|
||
|
||
#### Phase 2: Daten zuweisen
|
||
```sql
|
||
-- Bestehende Rezepte dem Migration User zuweisen
|
||
UPDATE recipes
|
||
SET created_by = 'migration-user-uuid'
|
||
WHERE created_by IS NULL;
|
||
|
||
-- Bestehende Notes dem Migration User zuweisen
|
||
UPDATE notes
|
||
SET user_id = 'migration-user-uuid'
|
||
WHERE user_id IS NULL;
|
||
|
||
-- Shopping Items vorerst ohne user_id lassen
|
||
-- Werden beim ersten User-Login zugewiesen
|
||
```
|
||
|
||
#### Phase 3: Erste echte User
|
||
- Erste User die sich registrieren bekommen alle bestehenden Daten zugewiesen
|
||
- Migration User wird nach erstem echten User gelöscht
|
||
- Shopping Items ohne user_id werden beim Login zugewiesen
|
||
|
||
### 6.3 Rollback Plan
|
||
|
||
- Auth-Features sind additiv, keine Breaking Changes
|
||
- Bei Problemen: Auth-Routes deaktivieren, App läuft weiter ohne Auth
|
||
- Datenbank-Rollback: user_id Spalten auf NULL setzen
|
||
|
||
## 7. Sicherheit
|
||
|
||
### 7.1 Passwort-Sicherheit
|
||
|
||
```javascript
|
||
// bcrypt mit 12 Rounds (Backend)
|
||
const passwordHash = await bcrypt.hash(password, 12)
|
||
|
||
// Passwort-Validierung (Frontend + Backend)
|
||
const passwordRules = {
|
||
minLength: 8,
|
||
requireUppercase: false, // UX-freundlich für v2
|
||
requireNumbers: false,
|
||
requireSpecialChars: false
|
||
}
|
||
```
|
||
|
||
### 7.2 JWT Security
|
||
|
||
```javascript
|
||
// JWT Signing (Backend)
|
||
const accessToken = jwt.sign(
|
||
{
|
||
sub: user.id,
|
||
email: user.email,
|
||
display_name: user.display_name,
|
||
household_id: user.household_id
|
||
},
|
||
process.env.JWT_SECRET,
|
||
{ expiresIn: '15m' }
|
||
)
|
||
|
||
// httpOnly Cookie Config
|
||
res.cookie('luna_refresh_token', refreshToken, {
|
||
httpOnly: true,
|
||
secure: process.env.NODE_ENV === 'production',
|
||
sameSite: 'strict',
|
||
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
|
||
})
|
||
```
|
||
|
||
### 7.3 Rate Limiting
|
||
|
||
```javascript
|
||
// Login Rate Limiting
|
||
const loginLimiter = rateLimit({
|
||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||
max: 5, // 5 attempts per IP
|
||
message: 'Zu viele Login-Versuche, bitte warten Sie 15 Minuten',
|
||
standardHeaders: true
|
||
})
|
||
|
||
// Registration Rate Limiting
|
||
const registerLimiter = rateLimit({
|
||
windowMs: 60 * 60 * 1000, // 1 hour
|
||
max: 3, // 3 registrations per IP per hour
|
||
message: 'Zu viele Registrierungen, bitte warten Sie'
|
||
})
|
||
```
|
||
|
||
### 7.4 Input Validation
|
||
|
||
```javascript
|
||
// Zod Schema für API Validation (Backend)
|
||
const registerSchema = z.object({
|
||
email: z.string().email('Ungültige E-Mail-Adresse'),
|
||
password: z.string().min(8, 'Passwort muss mindestens 8 Zeichen haben'),
|
||
display_name: z.string().min(2, 'Name muss mindestens 2 Zeichen haben')
|
||
.max(50, 'Name darf maximal 50 Zeichen haben')
|
||
})
|
||
|
||
// XSS Protection für User-generierte Inhalte
|
||
const sanitizedDisplayName = DOMPurify.sanitize(display_name)
|
||
```
|
||
|
||
## 8. Mobile-First UX Considerations
|
||
|
||
### 8.1 Touch Targets
|
||
- **Minimum Size:** 44px × 44px für alle Buttons
|
||
- **Spacing:** 8px minimum zwischen clickbaren Elementen
|
||
- **Form Fields:** 48px Höhe für bessere Touch-Ergonomie
|
||
|
||
### 8.2 Responsive Breakpoints
|
||
```css
|
||
/* Mobile First */
|
||
.auth-form {
|
||
width: 100%;
|
||
padding: 1rem;
|
||
}
|
||
|
||
/* Tablet */
|
||
@media (min-width: 768px) {
|
||
.auth-form {
|
||
max-width: 400px;
|
||
margin: 0 auto;
|
||
padding: 2rem;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 8.3 Keyboard & Input Handling
|
||
```jsx
|
||
// Input Types für bessere Mobile Keyboards
|
||
<input
|
||
type="email"
|
||
inputMode="email"
|
||
autoComplete="username"
|
||
placeholder="luna@example.com"
|
||
/>
|
||
|
||
<input
|
||
type="password"
|
||
autoComplete="current-password"
|
||
placeholder="Passwort"
|
||
/>
|
||
|
||
// Auto-focus Management
|
||
<input ref={emailRef} autoFocus onSubmit={focusPassword} />
|
||
```
|
||
|
||
### 8.4 Loading States
|
||
- **Skeleton Loading:** Für User Profile, Haushalt-Listen
|
||
- **Button States:** Loading Spinner in Buttons während API Calls
|
||
- **Toast Messages:** Für Success/Error Feedback
|
||
- **Optimistic Updates:** Favoriten, Haushalt beitreten
|
||
|
||
### 8.5 Offline Considerations
|
||
- **Service Worker:** Caching für Auth-relevante Pages
|
||
- **Network Awareness:** Retry-Mechanismus für failed Requests
|
||
- **Local Storage:** Backup für kritische User Preferences
|
||
|
||
## 9. Testing Strategy
|
||
|
||
### 9.1 Unit Tests
|
||
- Auth Context / Hook Tests
|
||
- Password Validation Tests
|
||
- JWT Token Handling Tests
|
||
- API Response Validation Tests
|
||
|
||
### 9.2 Integration Tests
|
||
- Login/Register Flow End-to-End
|
||
- Token Refresh Flow
|
||
- Haushalt erstellen/beitreten Flow
|
||
- Migration Logic Tests
|
||
|
||
### 9.3 Security Tests
|
||
- SQL Injection Prevention
|
||
- XSS Prevention
|
||
- CSRF Protection Validation
|
||
- Rate Limiting Effectiveness
|
||
|
||
## 10. Performance
|
||
|
||
### 10.1 Bundle Size
|
||
- **Code Splitting:** Auth-Pages lazy loaded
|
||
- **Tree Shaking:** Nur genutzte Auth-Libraries
|
||
- **JWT Library:** Lightweight jwt-decode statt vollständiger Library
|
||
|
||
### 10.2 API Optimization
|
||
- **Response Caching:** User Profile Daten
|
||
- **Debounced Requests:** Profile Updates
|
||
- **Batch Requests:** Initial App Load (User + Household in einem Call)
|
||
|
||
### 10.3 Database Performance
|
||
```sql
|
||
-- Wichtige Indizes für Auth Queries
|
||
CREATE INDEX idx_users_email ON users(email);
|
||
CREATE INDEX idx_household_members_user_id ON household_members(user_id);
|
||
CREATE INDEX idx_shopping_items_user_household ON shopping_items(user_id, household_id);
|
||
CREATE INDEX idx_user_favorites_user_id ON user_favorites(user_id);
|
||
```
|
||
|
||
---
|
||
|
||
## Implementierungs-Prioritäten
|
||
|
||
### Phase 1 (MVP)
|
||
1. ✅ Datenbank Schema + Migration
|
||
2. ✅ Backend Auth Endpoints
|
||
3. ✅ Frontend Login/Register Pages
|
||
4. ✅ JWT Token Flow + Auto-Refresh
|
||
5. ✅ Route Protection
|
||
|
||
### Phase 2 (Multi-User)
|
||
1. User Profile Management
|
||
2. Shopping Items User-Zuordnung
|
||
3. Favorites pro User
|
||
4. Notes pro User
|
||
|
||
### Phase 3 (Households)
|
||
1. Household Creation + Joining
|
||
2. Shared Shopping Lists
|
||
3. Household Management UI
|
||
4. Invite Code Generation
|
||
|
||
### Phase 4 (Polish)
|
||
1. Avatar Upload
|
||
2. Advanced Security Features
|
||
3. Performance Optimizations
|
||
4. Mobile UX Improvements
|
||
|
||
**Geschätzte Entwicklungszeit:** 3-4 Wochen für komplette Implementierung |