docs(bmad): PRD final + Architektur (Opus+qwen validiert), Addendum-Nachtraege

This commit is contained in:
Kenearos 2026-07-07 22:13:39 +02:00
parent 3fdc1b1dd0
commit 1b48dd1247
3 changed files with 107 additions and 3 deletions

View file

@ -0,0 +1,91 @@
# Architektur — Dienstplan-Pro Team-Release v1.0
*BMAD Phase 3 (Solutioning). Die „lean spine": Invarianten, aus denen alles Konsistente folgt. Leitet sich aus `prd-dienstplan-pro-2026-07-07/prd.md` + `addendum.md` ab. Brownfield — baut auf `project-context.md`.*
## 1. Architektur-Invarianten (nicht verhandelbar)
1. **Datentrennung ist server-erzwungen.** Jede Datenoperation ist auf `req.user.id` gefiltert. `user_id` stammt **ausschließlich** aus dem Session-Kontext, **nie** aus dem Client. Auch der Admin liest nur seine eigene Datenbasis.
2. **Kein Klartext-Geheimnis in der DB.** Login-Tokens und Session-IDs liegen nur als SHA-256-Hash vor; der Rohwert existiert nur im Link (einmalig) bzw. im httpOnly-Cookie.
3. **`foreign_keys = ON`** — sonst wirkt kein CASCADE. Löschen eines Nutzers räumt Tokens+Sessions atomar.
4. **Seed vor Migration.** `seedAdmin(ADMIN_EMAIL)` liefert `adminUserId`, **dann** `migrateToMultiUser(adminUserId)`. Fehlt/ungültig `ADMIN_EMAIL` → Fail-Fast, keine Migration.
5. **Migration ist eine einzige SQLite-Transaktion** (documents-Rebuild + history), idempotent (Guard: `documents.user_id` existiert schon?). Kein halb-migrierter Zustand.
6. **Auth ist additiv, nie im kritischen Offline-Pfad.** `401` (ungültige Session) → Login-Overlay; Netzwerkfehler (offline) → App läuft lokal weiter. Die beiden werden nie verwechselt.
7. **Same-origin.** Ein Node/Express-Prozess serviert Frontend + API. Kein CORS.
8. **better-sqlite3 ist synchron.** Niemals `db.run()`; immer `db.prepare().run()`/`db.exec()`. Keine `async` DB-Funktionen.
## 2. Komponenten
### Backend (`server/`)
| Datei | Verantwortung | Status |
|---|---|---|
| `db.js` | Verbindung, Pragmas (WAL, `foreign_keys=ON`), Schema, `getDoc(userId,key)`/`putDoc(userId,key,…)` | ändern |
| `auth.js` | `normalizeEmail`, `hashToken`, `createLoginToken`/`consumeLoginToken`, `createSession`/`validateSession`/`deleteSession`/`deleteUserSessions`, `seedAdmin`, `migrateToMultiUser`, `authMiddleware`, `adminMiddleware`, `listUsers`/`addUser`/`removeUser` (Last-Admin-Guard) | **neu** |
| `mailer.js` | `sendMagicLink(email,url)` via nodemailer; ohne SMTP → `console.log` (Dev) | **neu** |
| `ratelimit.js` | In-Memory `hit(key,limit,windowMin)`; E-Mail primär, IP großzügig | **neu** |
| `audit.js` | append-only Log (`ts,event,user_id,ip_hash`), keine PII | **neu** |
| `index.js` | Routen + Middleware + Cookie-Helfer; Startup-Sequenz (seed→migrate→listen) | ändern |
| `backup.js` | unverändert (tägliches Online-Backup) | — |
### Frontend
| Datei | Verantwortung | Status |
|---|---|---|
| `index.html` | Login-Overlay (Vollbild, initial sichtbar) + Bestätigungsseite-Route + Admin-Sektion/Logout in `#tab-settings` | ändern |
| `app.js` | Bootstrap: `GET /api/auth/me` vor App-Start; Offline≠401; Nutzerwechsel; Admin-UI-Logik | ändern |
| `sync.js` | `credentials:'include'`; 401→Overlay, Netzfehler→lokal; pending-Schutz | ändern |
| `storage.js` | `clearUserData()` (Daten-Keys + OpenRouter-Key), `dienstplan_current_user` | ändern |
| `auth-ui.js` | schlanke Login-/Bestätigungs-/Admin-Interaktion (neu, hält `app.js` fokussiert) | **neu** |
| `sw.js` | Cache-Version bump | ändern |
## 3. Datenmodell
```
users(id PK, email UNIQUE [normalisiert], is_admin, created_at)
login_tokens(token_hash PK, user_id FK→users ON DELETE CASCADE, expires_at, used_at)
sessions(id_hash PK, user_id FK→users ON DELETE CASCADE, expires_at, created_at, last_seen_at)
documents(user_id, key, value, updated_at, PRIMARY KEY(user_id,key)) -- Rebuild
history(…, user_id) -- ADD COLUMN
audit_log(id PK, ts, event, user_id, ip_hash) -- neu, keine PII
```
`last_seen_at` trägt den Idle-Timeout (FR-3): bei jeder Auth-geschützten Anfrage aktualisiert; `validateSession` prüft `now-last_seen_at < SESSION_IDLE_HOURS` **und** `now < expires_at`.
## 4. Kern-Flows
- **Login-Request:** `POST /api/auth/request``ratelimit(email)``normalizeEmail` → user in Allowlist? → alte offene Tokens invalidieren → `createLoginToken``sendMagicLink`**immer** neutrale 200.
- **Einlösung (scanner-sicher):** `GET /auth?token=` → Bestätigungsseite (kein Verbrauch). `POST /auth/confirm``consumeLoginToken` (Hash-Vergleich, unused, <30min, `used_at` setzen) `createSession` Cookie 302 `/`.
- **Session-Check:** `authMiddleware` liest Cookie → `validateSession` (Hash→sessions JOIN users, expires + idle) → `req.user`; sonst 401 + Cookie clear. `last_seen_at` bump.
- **Migration (Startup):** Fail-Fast `ADMIN_EMAIL``seedAdmin``migrateToMultiUser(adminId)` (eine Transaktion) → `listen`.
- **Nutzerwechsel (Frontend):** `me.email``dienstplan_current_user` (oder keiner) → `clearUserData()``DataSync.boot()`.
- **Admin entfernt Nutzer:** `DELETE /api/admin/users/:id` → Last-Admin-Guard → delete user → CASCADE räumt tokens/sessions → audit.
## 5. Sicherheits-Architektur
Hashing (Token+Session) · httpOnly/Secure/SameSite=Lax · Einmal-Token 30min + neutrale Antwort + Rate-Limit (E-Mail primär) · scanner-sichere POST-Einlösung · Idle-Timeout · Token in Access-Logs maskieren (Caddy) · Audit-Log ohne PII · `user_id` nur aus Session (Anti-IDOR).
## 6. Technologie-Entscheidungen
- **nodemailer** (einzige neue Dependency) für SMTP; Konsolen-Fallback.
- **Kein cookie-parser**`res.cookie()` nativ, Lesen via Mini-Helfer.
- **Server-Sessions** (widerrufbar) statt JWT.
- **Rate-Limit in-memory** (per-Prozess; für einen Container ok — Ceiling dokumentiert).
## 7. Test-Architektur
Node-`--test`: `auth.test.js` (hash/consume/session/migration/last-admin/normalizeEmail/anti-IDOR), `ratelimit.test.js`, `state.test.js` (2-Nutzer-Trennung + 401/403). Browser-E2E (wie Stufe 1): kompletter Login/Trennung/Logout/Nutzerwechsel-Durchlauf mit Konsolen-Magic-Link.
## 8. Deployment-Architektur
Docker `node:20-slim` hinter Caddy, Volume `dienstplan-data:/data`. Neue Env: `ADMIN_EMAIL, APP_BASE_URL, SESSION_TTL_DAYS=30, SESSION_IDLE_HOURS=8, RATE_LIMIT_EMAIL=5, RATE_LIMIT_WINDOW_MIN=15, RATE_LIMIT_IP=…, SMTP_*`. SPF/DKIM Voraussetzung. Deploy-Guard prüft `ADMIN_EMAIL` vor Live.
## 9. Verfeinerungen (qwen-Architektur-Gate)
**Übernommen:**
- **`cookie-parser`** statt handgerolltem Parsing (2. Dependency neben `nodemailer`) — vermeidet Encoding-/Flag-Fehler. Setzen weiterhin via `res.cookie()`.
- **Gedrosselter `last_seen_at`-Write:** nur aktualisieren, wenn älter als ~5 Min → kein Write pro Request, keine Lock-Contention.
- **FK-Indizes:** `CREATE INDEX` auf `login_tokens.user_id` und `sessions.user_id` (schnelles CASCADE/Cleanup).
- **CSRF + Fixierungsschutz:** Die Bestätigungs-POST (`/auth/confirm`) trägt den Token aus der Seite (CSRF-Angreifer hat ihn nicht) + ein Form-CSRF-Token; die Session wird **neu** erst bei erfolgreichem Consume erzeugt (kein Cookie vor Confirm → keine Session-Fixation).
**Bewusst akzeptiert (Team-Skala, Ceiling dokumentiert):**
- better-sqlite3 synchron blockiert den Event-Loop minimal — bei einstelliger Nutzerzahl irrelevant; kein Web-Scale-Locking-Thema.
- Rate-Limit in-memory (resettet bei Restart) — bei Magic-Link ohne Passwort kaum Brute-Force-Fläche; Persistenz = YAGNI.
- Migration auf kleinem Datenbestand (hunderte Dienste) — kein OOM/Tx-Log-Thema.
- Login-Token-Race bei Parallel-Klick: neuster Link gewinnt, tolerierbar.

View file

@ -57,3 +57,14 @@ Cookie setzen: `res.cookie('session', raw, {httpOnly:true, secure:true, sameSite
## Verifikation (Kurz)
Node-Unit-Tests (Token-Hash/Consume, Session validate/delete, Migration idempotent, Rate-Limit, Datentrennung 2 Nutzer, 401/403). Browser-E2E: Server ohne SMTP → Magic-Link in Konsole → kompletter Login-/Trennungs-/Logout-/Nutzerwechsel-Durchlauf. Migration gegen Prod-DB-Kopie.
## Review-Nachträge (Opus-4-Linsen + qwen)
- **Migrations-Atomarität** ist bei **SQLite** real gegeben (echte Transaktion inkl. DDL) — kein Datei-Rollback nötig; Guard über `documents.user_id`-Existenz. Migration + history-Update in **einer** Transaktion, sonst Teilabbruch-Risiko (verwaiste history).
- **Session-Invalidierung bei Nutzer-Löschung** funktioniert per `sessions.user_id`-FK + CASCADE (kein Reverse-Lookup vom Hash nötig — user_id ist indiziert). Logout löscht per Hash des präsentierten Cookies.
- **`user_id` nur aus Session:** In `authMiddleware` gesetzt; `/api/state` liest `req.user.id`, ignoriert jeden client-seitigen `user_id`. Eigener Test: manipulierte `user_id` im Body/Query hat keinen Effekt.
- **E-Mail-Normalisierung** = eine Funktion `normalizeEmail(lowercase+trim)`, identisch bei `seedAdmin`, Allowlist-Insert (FR-9) und Login-Request-Abgleich (FR-1).
- **Audit-Log:** append-only, Felder `ts, event, user_id (pseudonym), ip_hash?`**keine E-Mail/Namen**. Events: login_ok, logout, admin_add/remove, emergency_link, auth_fail. Macht SM-1 (distinct user_id) und SM-3 (auth_fail-Häufung) auswertbar.
- **Deploy-Guard:** Pipeline/Startup prüft `ADMIN_EMAIL` gesetzt+valide **vor** Live (Fail-Fast in `seedAdmin`, zusätzlich Deploy-Checkliste).
- **OpenRouter-Key beim Nutzerwechsel:** wird geleert (Kosten-/Secret-Schutz auf geteiltem PC). Bewusste Abwägung gegen Komfort; auf Einzelgeräten neu einzugeben.
- **Foto-Import-Ambiguität** (zwei ähnliche Namen): bestehende Levenshtein-≤2-Heuristik; bei Mehrdeutigkeit besser als „unbekannt" behandeln (Nutzer entscheidet in der Vorschau) — bestehende Funktion, keine v1-Änderung erzwungen.

View file

@ -1,6 +1,6 @@
---
title: Dienstplan-Pro
status: draft
status: final
created: 2026-07-07
updated: 2026-07-07
---
@ -120,6 +120,7 @@ Fällt die Mail-Zustellung aus, kann ein Admin einem freigeschalteten Nutzer ein
**Consequences (testable):**
- Ein Admin kann für eine freigeschaltete E-Mail einen einmaligen Login-Link erzeugen/abrufen (übergibt ihn selbst, z.B. per Arbeits-Chat).
- Der Link unterliegt denselben Regeln wie FR-2 (Bestätigung, Ablauf, Einmaligkeit).
- Jede Not-Link-Erzeugung wird als Admin-Aktion im Audit-Log verzeichnet (Missbrauchs-Nachvollziehbarkeit, siehe §6.1).
### 4.2 Nutzer-Datentrennung
@ -128,6 +129,7 @@ Fällt die Mail-Zustellung aus, kann ein Admin einem freigeschalteten Nutzer ein
#### FR-6: Strikte Datentrennung pro Nutzer
**Consequences (testable):**
- `GET/PUT /api/state` betrifft ausschließlich die Dokumente des eingeloggten Nutzers — auch für Admins (Admin ist **kein** Daten-Superuser).
- Die `user_id` wird **ausschließlich aus dem Session-Kontext** des Servers abgeleitet, **nie** aus dem Client-Payload — Test gegen horizontale Rechteausweitung (manipulierte `user_id` im Request hat keinen Effekt).
- Zwei verschiedene Nutzer sehen disjunkte Datensätze (stehender 2-Nutzer-Isolationstest, siehe SM-2).
#### FR-7: Übernahme der Alt-Daten (sicher & verifiziert)
@ -150,7 +152,7 @@ Beim Nutzerwechsel/Erstlogin im selben Browser werden keine Daten vermischt. Rea
**Description:** Der Admin pflegt die Allowlist in einem nur für ihn sichtbaren Bereich. Realizes UJ-4.
#### FR-9: E-Mail freischalten
**Consequences (testable):** Nach Freischalten kann diese E-Mail einen Magic-Link erhalten (FR-1); E-Mails normalisiert (lowercase/trim); Duplikate erzeugen keinen zweiten Nutzer.
**Consequences (testable):** Nach Freischalten kann diese E-Mail einen Magic-Link erhalten (FR-1); E-Mails werden mit **identischer Normalisierung** (lowercase/trim) sowohl beim Speichern als auch beim Allowlist-Abgleich behandelt (case-insensitive, keine Lücke); Duplikate erzeugen keinen zweiten Nutzer.
#### FR-10: Nutzerliste einsehen
**Consequences (testable):** Liste zeigt alle Nutzer inkl. Admin-Kennzeichnung.
@ -207,7 +209,7 @@ Beim Nutzerwechsel/Erstlogin im selben Browser werden keine Daten vermischt. Rea
- Auth-Gate auf `/api/state`; auth-bewusster, offline-fähiger Frontend-Sync; Nutzerwechsel-/Erstlogin-Isolation (inkl. OpenRouter-Key + pending-Schutz).
- E-Mail-Versand per SMTP (Konsolen-Fallback im Dev/Test).
- Sicherheits-Härtungen: Token- & Session-Hashing, `foreign_keys`+CASCADE, Rate-Limit (E-Mail primär, IP großzügig), Cookie-Flags, Token in Access-Logs maskiert.
- **Minimales Audit-Log** (Login/Logout/Admin-Aktionen/401-403-Häufungen, **ohne PII**) — macht die Metriken beobachtbar.
- **Minimales Audit-Log** (Login/Logout/Admin-Aktionen inkl. Not-Link/401-403-Häufungen) — **ohne PII**, aber mit **stabiler pseudonymer Nutzer-Kennung** (`user_id`, nicht E-Mail), damit SM-1 „≥3 verschiedene Nutzer" zählbar ist.
- Release-Hygiene: `.env.example`, `README`-Update, `CHANGELOG`, Version-Tag `v1.0.0`, veraltete `CLAUDE.md`-Regeln aktualisieren, alte Test-Artefakte prüfen/aufräumen.
- **Bericht-Text-Splice-Bug** — eigenes Arbeitspaket mit Akzeptanzkriterium: im Textbericht dürfen Bemerkungs-Zeilen zweier Mitarbeiter nicht mehr ineinanderrutschen (jede Bemerkung dem korrekten Mitarbeiter zugeordnet).