Files
CapaKraken/plan.md
T
Hartmut 435c871e1f security: implement tickets #28-#35 + architecture decision #30
#28 - TOTP rate limiting (verifyTotp): added totpRateLimiter (10 req/30s),
  throws TOO_MANY_REQUESTS before DB hit; 16 unit tests including rate-limit
  exceeded + userId key isolation.

#29 - /api/reports/allocations role check: only ADMIN/MANAGER/CONTROLLER may
  access; returns 403 otherwise; 9 unit tests (401 unauthenticated, 403 for
  USER/VIEWER, 200 for allowed roles + xlsx format).

#31 - pgAdmin credentials moved out of docker-compose.yml into env vars;
  PGADMIN_PASSWORD is now required (:?) to prevent accidental plaintext
  exposure in committed files.

#34 - Server-side HTML sanitization for comment bodies via stripHtml():
  strips all tags + decodes safe entities before persistence; 16 unit tests
  covering passthrough, injection patterns, entity decoding.

#35 - MFA setup prompt banner (MfaPromptBanner): shown to ADMIN/MANAGER users
  without TOTP enabled; user-scoped localStorage snooze (7 days); links to
  /account/security; accessibility role=alert; 7 structural unit tests.

#33 - Auth anomaly alerting cron (/api/cron/auth-anomaly-check): detects
  HIGH_GLOBAL_FAILURE_RATE and CONCENTRATED_FAILURES in 30-minute window;
  CRITICAL notification to ADMINs; fail-closed via verifyCronSecret;
  10 unit tests.

#32 - MFA enforcement policy: added requireMfaForRoles field to SystemSettings
  schema + Prisma migration; auth.ts blocks login with MFA_REQUIRED_SETUP
  signal if role is enforced but TOTP not set up; signin page redirects to
  /account/security?mfa_required=1; settings schema + view model updated;
  11 unit tests.

#30 - API keys architecture decision documented in LEARNINGS.md; no code
  written — product decision required before implementation.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-04-01 23:25:06 +02:00

473 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CapaKraken — Umsetzungsplan
Gitea-Repo: `https://gitea.hartmut-noerenberg.com/Hartmut/plANARCHY`
Stand: 2026-04-01 | Issues: #19#35
---
## Plan: Security-Tickets #28#35 — OWASP-Härtung Round 2
### Anforderungsanalyse
8 offene Security-Tickets aus dem OWASP-Audit. Pro Ticket: Implementation + Unit-Tests (happy path + negative/edge cases) + E2E-Tests wo sinnvoll.
**Reihenfolge nach Risiko und Aufwand:**
| Ticket | Thema | Severity | Aufwand |
|--------|-------|----------|---------|
| #28 | TOTP `verifyTotp` Rate Limiting | High | S |
| #29 | `/api/reports/allocations` Rollencheck | Medium | S |
| #31 | pgAdmin Credentials docker-compose | Medium | XS |
| #34 | Kommentar server-seitige Sanitierung | Low | S |
| #35 | MFA-Setup-Prompt für Admins beim Login | UX | M |
| #33 | Auth-Anomaly-Alerting Cron | Medium | M |
| #32 | MFA Enforcement Policy (hard) | Medium | L |
| #30 | API-Keys in DB (Design Decision) | Medium | — |
### Betroffene Pakete & Dateien
| Paket | Datei | Art |
|-------|-------|-----|
| `packages/api` | `src/middleware/rate-limit.ts` | edit — totpRateLimiter export |
| `packages/api` | `src/router/user-self-service-procedure-support.ts` | edit — verifyTotp rate limit |
| `packages/api` | `src/__tests__/user-self-service-mfa.test.ts` | edit — rate limit tests |
| `apps/web` | `src/app/api/reports/allocations/route.ts` | edit — role check |
| `apps/web` | `src/app/api/reports/allocations/route.test.ts` | create |
| `docker-compose.yml` | — | edit — pgAdmin credentials |
| `packages/api` | `src/router/comment-procedure-support.ts` | edit — sanitizeBody |
| `packages/api` | `src/lib/html-sanitize.ts` | create — server-side sanitizer |
| `packages/api` | `src/__tests__/comment-sanitize.test.ts` | create |
| `apps/web` | `src/components/dashboard/MfaPromptBanner.tsx` | create — UX prompt |
| `apps/web` | `src/app/(app)/dashboard/page.tsx` | edit — server-side MFA check |
| `apps/web` | `src/app/(app)/account/security/page.tsx` | edit — mfa-prompt param |
| `apps/web` | `src/app/api/cron/auth-anomaly-check/route.ts` | create |
| `packages/db` | `prisma/schema.prisma` | edit — SystemSettings.requireMfaForRoles |
| `apps/web` | `src/server/auth.ts` | edit — MFA enforcement check |
| `apps/web` | `src/app/(app)/admin/settings/page.tsx` | edit — MFA policy UI |
### Task-Liste
**#28 — TOTP Rate Limiting**
- [x] `totpRateLimiter` in `rate-limit.ts` exportieren: 10 Versuche / 30s
- [x] `verifyTotp()` wirft 429 wenn Rate Limit überschritten
- [x] Unit-Tests: valid pass, 10x fehlgeschlagen → 429, verschiedene UserIds unabhängig
**#29 — Allocations Role Check**
- [x] `auth()` + session.user.role prüfen: nur CONTROLLER/MANAGER/ADMIN
- [x] Unit/Integration-Test: USER → 403, MANAGER → 200
**#31 — pgAdmin Credentials**
- [x] `PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin}` → remove default, require explicit
**#34 — Comment Sanitization**
- [x] `src/lib/html-sanitize.ts` — Strip-only sanitizer (kein HTML in Comments erlaubt)
- [x] `createComment` + `updateComment` sanitize body vor DB-Write
- [x] Unit-Tests: XSS-Payload, Script-Tag, sauberer Text bleibt erhalten
**#35 — MFA Prompt**
- [x] Dashboard-Page (Server): session role + DB totpEnabled check → prop an Client
- [x] `MfaPromptBanner.tsx` — "Set up MFA" / "Remind me later" (7-Tage LocalStorage)
- [x] Security-Page: `?mfa-prompt=1` → Banner + direkter Start des Setup-Flows
- [x] E2E-Test: Admin ohne MFA → Banner sichtbar; nach "Später" → Banner weg für 7 Tage
**#33 — Auth Anomaly Alerting**
- [x] `/api/cron/auth-anomaly-check/route.ts` — AuditLog aggregation, Notification bei Spike
- [x] Unit-Test: Aggregationslogik isoliert
**#32 — MFA Enforcement**
- [x] Prisma: `requireMfaForRoles String[]` auf `SystemSettings`
- [x] `auth.ts` `authorize`: wenn Rolle in `requireMfaForRoles` + kein MFA → throw MFA_SETUP_REQUIRED
- [x] Admin-Settings-UI: Multiselect für Pflicht-Rollen
- [x] E2E-Test: enforcement + bypass nach MFA-Setup
**#30 — API Keys (Design Decision)**
- [ ] Architektur-Entscheidung in LEARNINGS.md dokumentieren (Option A vs B)
- [ ] Kein Code ohne Entscheidung
### Abhängigkeiten
- #35 (MFA Prompt) sollte vor #32 (Enforcement) fertig sein
- #28, #29, #31, #34 sind unabhängig voneinander
### Akzeptanzkriterien
- [ ] `pnpm test:unit` grün (alle Pakete)
- [ ] `pnpm --filter @capakraken/web exec tsc --noEmit` — keine neuen Errors
- [ ] Alle Tickets kommentiert + `in-review` gesetzt
- [ ] Commit auf main
---
## Plan: Ticket #27 — Dashboard Widget Bug (new users see empty modal)
### Anforderungsanalyse
Neue Nutzer können keine Widgets zum Dashboard hinzufügen. Root cause: `localStorage`-Key
`"capakraken_dashboard_v1"` ist **nicht user-scoped**. Wenn User A sich ausloggt und User B
auf demselben Gerät anmeldet, liest `useDashboardLayout` das Layout von User A aus
`localStorage`. Hat User A ein leeres Dashboard gespeichert, sieht User B 0 Widgets —
und nach Klick auf "Add Widget" im Modal erscheint der neue Widget **nicht** (da der
Hydrationszustand nicht sauber initialisiert wird).
**Fix:** localStorage-Key auf `capakraken_dashboard_v1_{userId}` umstellen. Vor dem Zugriff
auf `localStorage` wartet der Hook auf `trpc.user.me`, um die User-ID zu kennen.
### Betroffene Pakete & Dateien
| Paket | Datei | Art |
|-------|-------|-----|
| `apps/web` | `src/hooks/useDashboardLayout.ts` | edit — user-scoped storage key |
| `apps/web` | `e2e/dev-system/dashboard-widgets.spec.ts` | create — E2E tests |
### Task-Liste
- [x] **T-1:** `useDashboardLayout` auf user-scoped localStorage umstellen → `useDashboardLayout.ts`
- [x] **T-2:** E2E-Test für den kompletten Widget-Flow (new user, add, persist, reload) → `dashboard-widgets.spec.ts`
- [ ] **T-3:** `pnpm --filter @capakraken/web exec tsc --noEmit` — keine neuen TS-Fehler
- [ ] **T-4:** E2E-Tests gegen laufenden Dev-Server ausführen (manuell oder CI)
- [ ] **T-5:** Commit + Ticket #27 auf Gitea kommentieren + `in-review` setzen
### Akzeptanzkriterien
- [ ] Neuer Admin-User sieht nach Login das Default-Dashboard (stat-cards)
- [ ] Modal zeigt alle 11 Widgets an
- [ ] Gewählter Widget erscheint sofort auf dem Dashboard
- [ ] Nach Page-Reload ist der Widget weiterhin sichtbar
- [ ] Cross-User-Bleed: User B erbt nicht das Layout von User A
---
## Plan: In-Review-Tickets verifizieren und in main integrieren
### Anforderungsanalyse
8 Tickets sind `in-review`. Ziel: jeden Ticket-Scope im Code verifizieren, Gitea-Kommentar
mit Bewertung (✅ akzeptiert / ⚠️ Nachbesserung nötig) hinterlassen, dann alle offenen
Changes in einem sauberen Commit auf `main` integrieren.
**Umfang der Verifikation pro Ticket:**
- Code-Artefakte im Repo prüfen (Dateien, Tests)
- Quality-Gates (tsc, test:unit) müssen grün sein
- Akzeptanzkriterien aus dem Ticket-Body abgleichen
- Gitea-Kommentar: Strukturiertes Urteil mit Befunden
**Integration-Strategie:**
- Alle ungestagten Änderungen (`git status`) gehören zu den Tickets #20, #22, #25, #26
- Ein einziger Commit auf `main` mit klarer Commit-Message
- plan.md und docs/ mitcommiten
### Betroffene Pakete & Dateien
| Paket | Dateien | Art der Änderung |
|-------|---------|-----------------|
| `apps/web` | `next.config.ts`, `src/app/layout.tsx`, `src/middleware.ts`, `src/middleware.test.ts`, `src/app/api/perf/route.ts`, `src/app/api/perf/route.test.ts`, `src/components/security/MfaSetup.test.ts`, `e2e/dev-system/rbac-data-access.spec.ts`, `e2e/dev-system/helpers.ts` | Verifikation + commit |
| `packages/api` | `src/__tests__/ssrf-guard.test.ts`, `src/__tests__/webhook-procedure-support.test.ts` | Verifikation + commit |
| root | `package.json`, `docker-compose.yml`, `docs/developer-runbook.md`, `plan.md` | Verifikation + commit |
### Task-Liste (atomare Schritte in Reihenfolge)
**Phase 1 — Verifikation & Kommentierung (sequenziell, ein Ticket nach dem anderen)**
- [ ] **V-1:** Ticket #19 (MFA-QR) — Code-Check: `MfaSetup.tsx` + `MfaSetup.test.ts`; Urteil auf Gitea posten
- [ ] **V-2:** Ticket #20 (Webhook SSRF) — Code-Check: `ssrf-guard.ts` + `ssrf-guard.test.ts` + `webhook-procedure-support.test.ts`; Urteil posten
- [ ] **V-3:** Ticket #21 (/api/perf) — Code-Check: `route.ts` + `route.test.ts`; Urteil posten
- [ ] **V-4:** Ticket #22 (CSP) — Code-Check: `middleware.ts` + `middleware.test.ts` + `next.config.ts` + `layout.tsx`; Urteil posten
- [ ] **V-5:** Ticket #23 (Active-Session-Registry) — Code-Check: Session-Guard-Middleware, Auth-Flow; Urteil posten
- [ ] **V-6:** Ticket #24 (Docker reproducibility) — Code-Check: `docker-compose.yml`, Dockerfile.dev; Urteil posten
- [ ] **V-7:** Ticket #25 (Env/Migration-Strategie) — Code-Check: `docker-compose.yml`, `docs/developer-runbook.md`; Urteil posten
- [ ] **V-8:** Ticket #26 (RBAC E2E-Tests) — Code-Check: `rbac-data-access.spec.ts`; Urteil posten
**Phase 2 — Quality Gates**
- [ ] **Q-1:** `pnpm --filter @capakraken/web exec tsc --noEmit` — keine Fehler
- [ ] **Q-2:** `pnpm --filter @capakraken/api test:unit` — alle Tests grün
- [ ] **Q-3:** `pnpm --filter @capakraken/web test:unit` — alle Tests grün
**Phase 3 — Git-Integration**
- [ ] **G-1:** `git add` aller ungestagten Änderungen (alle Ticket-Artefakte)
- [ ] **G-2:** Commit mit Message: `security/platform: close audit findings #19#26 (tests, CSP nonce, SSRF guard, runbook)`
- [ ] **G-3:** `git push origin main`
### Abhängigkeiten
- V-1 bis V-8 sind unabhängig voneinander, aber sequenziell (ein Kommentar pro Ticket)
- Q-1 bis Q-3 können nach allen V-Tasks parallel laufen
- G-1/G-2/G-3 müssen nach Q-1..3 erfolgen (kein Commit bei roten Tests)
### Akzeptanzkriterien
- [ ] Alle 8 in-review-Tickets haben einen Bewertungskommentar
- [ ] `pnpm test:unit` läuft grün (beide Pakete)
- [ ] `tsc --noEmit` ohne Fehler
- [ ] Alle neuen Dateien in einem sauberen Commit auf `main`
- [ ] `git status` danach: working tree clean
### Risiken & offene Fragen
- **Ticket #23 (Active-Session):** Die Implementierung in früheren Sessions könnte durch spätere Auth-Fixes (jti→sid) überholt worden sein — genau prüfen
- **Ticket #24 vs. #25:** Überlappen in `docker-compose.yml` — beide Tickets betreffen dieselbe Datei; ein Commit deckt beide ab
- **Push auf main:** Direkt auf `main` — kein PR da kein Remote-Review-Prozess konfiguriert. Sicherstellen dass alle Tests grün sind
---
---
## Ticket #25 — Docker/Env/Migration-Strategie
### Anforderungsanalyse
Ziel: Docker-Container-Lifecycle ohne manuelle Eingriffe.
Konkrete Mängel:
1. `REDIS_URL` in `docker-compose.yml` nutzt `${REDIS_URL:-redis://redis:6379}` — Host-Env-Var kann Docker-internen Wert überschreiben (gleiche Klasse wie das behobene `DATABASE_URL`-Problem)
2. Migration-Strategy undokumentiert: DB per `db push` aufgebaut, dann Migration hinzugefügt → `migrate deploy` scheitert mit P3005, erforderte `migrate resolve --applied`
3. Kein Developer-Runbook (Setup, Restart, DB-Ops fehlen)
### Betroffene Dateien
| Datei | Änderung |
|---|---|
| `docker-compose.yml` | `REDIS_URL` hardcoden |
| `docs/developer-runbook.md` | create |
### Task-Liste
- [ ] **#25-T1:** `docker-compose.yml``REDIS_URL: redis://redis:6379` (Literal, kein `${}`)
- [ ] **#25-T2:** `docs/developer-runbook.md` erstellen mit: Erstmaligem Setup, DB-Migration-Strategie inkl. P3005-Recovery, E2E_TEST_MODE-Erklärung, Container-Neustart-Checkliste
---
## Ticket #26 — RBAC Datenzugriffs-Matrix E2E-Tests
### Anforderungsanalyse
Neue Testdatei `apps/web/e2e/dev-system/rbac-data-access.spec.ts` mit **Netzwerk-Ebene** tRPC-Response-Assertions (nicht nur UI-Sichtbarkeit).
Grundlage `docs/route-access-matrix.md`:
| tRPC-Prozedur | Audience | Admin | Manager | Viewer |
|---|---|---|---|---|
| `user.list` | `admin-only` | ✓ 200 | FORBIDDEN | FORBIDDEN |
| `allocation.listView` | `planning-read` | ✓ 200 | ✓ 200 | FORBIDDEN |
| `resource.listSummaries` | `resource-overview` | ✓ 200 | ✓ 200 | FORBIDDEN |
| `user.listAssignable` | `manager-write` | ✓ 200 | ✓ 200 | FORBIDDEN |
Technik: `page.evaluate()` mit `fetch()` gegen `/api/trpc/<proc>?batch=1&input=...` — läuft im Browser-Kontext der gespeicherten Session.
tRPC GET-Format:
```
GET /api/trpc/<proc>?batch=1&input={"0":{"json":null}}
Erfolg: [{"result":{"data":{"json":[...]}}}]
Fehler: [{"error":{"json":{"data":{"code":"FORBIDDEN","httpStatus":403}}}}]
```
### Betroffene Dateien
| Datei | Änderung |
|---|---|
| `apps/web/e2e/dev-system/rbac-data-access.spec.ts` | create |
### Task-Liste
- [ ] **#26-T1:** Datei erstellen mit `trpcQuery(page, procedure, input?)` Helper-Funktion
- [ ] **#26-T2:** Admin-Describe-Block (4 Tests: alle 4 Prozeduren → Erfolg erwartet)
- [ ] **#26-T3:** Manager-Describe-Block (4 Tests: `user.list` → FORBIDDEN, Rest → Erfolg)
- [ ] **#26-T4:** Viewer-Describe-Block (4 Tests: alle → FORBIDDEN)
- [ ] **#26-T5:** Smoke-Run: `pnpm exec playwright test e2e/dev-system/rbac-data-access.spec.ts --config=playwright.dev.config.ts`
### Risiken
- `allocation.listView` könnte ein Pflicht-Input-Objekt erfordern → Falls `BAD_REQUEST` statt FORBIDDEN, Schema im Router prüfen und minimalen Input übergeben
- Viewer-Permissions aus DB-Seed (SystemRoleConfig) — prüfen ob VIEWER tatsächlich kein `VIEW_PLANNING` hat
---
---
## Anforderungsanalyse
Alle 6 Issues stammen aus einem Security-Audit und einem Plattform-Review.
5 davon sind Security-Findings (OWASP A02/A05/A07/A09), 1 ist ein Plattform-Thema (Docker-Reproduzierbarkeit).
Die Security-Issues sind weitgehend unabhängig voneinander; lediglich #23 greift in den tRPC-Request-Pfad ein und sollte zuletzt umgesetzt werden, da es den Haupt-Auth-Pfad berührt.
---
## Betroffene Pakete & Dateien
| Issue | Paket/Pfad | Datei | Art |
|-------|-----------|-------|-----|
| #21 | `apps/web` | `src/app/api/perf/route.ts` | edit |
| #19 | `apps/web` | `src/components/security/MfaSetup.tsx` | edit |
| #19 | `apps/web` | `package.json` | edit (neue Dep: `qrcode` + `@types/qrcode`) |
| #20 | `packages/api` | `src/lib/webhook-dispatcher.ts` | edit |
| #20 | `packages/api` | `src/router/webhook-support.ts` | edit |
| #20 | `packages/api` | `src/lib/ssrf-guard.ts` | create |
| #22 | `apps/web` | `next.config.ts` | edit |
| #23 | `apps/web` | `src/app/api/trpc/[trpc]/route.ts` | edit |
| #23 | `apps/web` | `src/server/auth.ts` | edit (Doku/Cleanup) |
| #24 | root | `Dockerfile.dev`, `Dockerfile.prod`, `docker-compose.yml`, `docker-compose.prod.yml`, `tooling/docker/app-dev-start.sh` | edit |
---
## Task-Liste (in empfohlener Reihenfolge)
### Issue #21 — /api/perf fail-closed + Query-Token entfernen
- [ ] **Task 1:** `GET`-Handler in `apps/web/src/app/api/perf/route.ts` ändern:
- `if (cronSecret)``if (!cronSecret) return 401/403` (fail-closed)
- `queryToken`-Zweig vollständig entfernen
- Nur noch `Authorization: Bearer <secret>` prüfen
- → Datei: `apps/web/src/app/api/perf/route.ts`
- [ ] **Task 2:** Unit-Tests für `/api/perf`:
- Test: autorisiert per Header → 200
- Test: kein Secret → 401
- Test: Query-Param-Token → 401 (nicht mehr akzeptiert)
- Test: fehlende `CRON_SECRET`-Env → fail-closed (kein Metrics-Leak)
- → Datei: `apps/web/src/app/api/perf/route.test.ts` (neu)
---
### Issue #19 — MFA QR lokal rendern
- [ ] **Task 3:** `qrcode`-Paket und `@types/qrcode` zu `apps/web/package.json` hinzufügen, `pnpm install` ausführen.
- [ ] **Task 4:** `MfaSetup.tsx` umschreiben:
- `<img src="https://api.qrserver.com/...">` durch lokale QR-Generierung ersetzen
- `qrcode.toDataURL(uri)` im Client-Effekt aufrufen und als `<img src={dataUrl}>` rendern
- Sicherstellen: der `otpauth://`-URI verlässt den Browser nicht mehr
- → Datei: `apps/web/src/components/security/MfaSetup.tsx`
- [ ] **Task 5:** Test sicherstellen, dass kein Rendering-Request an externe QR-URL geht:
- Unit-Test oder Playwright-Test der prüft, dass kein `<img src>` mit `qrserver.com` oder `chart.googleapis.com` gerendert wird
- → Datei: `apps/web/src/components/security/MfaSetup.test.tsx` (neu oder bestehend ergänzen)
---
### Issue #20 — Webhook SSRF-Schutz
- [ ] **Task 6:** `ssrf-guard.ts` erstellen mit einer `assertWebhookUrlAllowed(url: string): void`-Funktion:
- Parst die URL, löst Hostname auf (DNS-Check via Node `dns.lookup`)
- Blockt: Loopback (`127.0.0.0/8`, `::1`), RFC1918 (`10.x`, `172.1631.x`, `192.168.x`), Link-Local (`169.254.x`), Cloud-Metadata (`169.254.169.254`)
- Blockt: alle Schemes außer `https` (und `http` nur wenn expliziter Dev-Override gesetzt)
- Wirft `TRPCError({ code: "BAD_REQUEST" })` mit allgemeiner Fehlermeldung (ohne IP preiszugeben)
- → Datei: `packages/api/src/lib/ssrf-guard.ts`
- [ ] **Task 7:** `ssrf-guard` in `webhook-support.ts` und `webhook-dispatcher.ts` integrieren:
- Vor Speicherung + vor Dispatch `assertWebhookUrlAllowed(url)` aufrufen
- → Dateien: `packages/api/src/router/webhook-support.ts`, `packages/api/src/lib/webhook-dispatcher.ts`
- [ ] **Task 8:** Unit-Tests für `ssrf-guard.ts`:
- Erlaubt: `https://example.com/hook`
- Blockt: `http://localhost/…`, `http://127.0.0.1/…`, `http://10.0.0.1/…`, `http://192.168.1.1/…`, `http://169.254.169.254/…`, `ftp://…`
- → Datei: `packages/api/src/__tests__/ssrf-guard.test.ts` (neu)
---
### Issue #22 — CSP härten
- [ ] **Task 9:** CSP in `apps/web/next.config.ts` überarbeiten:
- `unsafe-eval` entfernen oder nur für `NODE_ENV === "development"` erlauben
- `unsafe-inline` aus `script-src` entfernen
- Nonce-basierte Inline-Scripts prüfen: Next.js 15 unterstützt CSP-Nonces via `nonce`-Prop auf `<Script>` — recherchieren ob tatsächliche Inline-Scripts existieren, die Nonces brauchen
- Unnötige `connect-src`-Origins bereinigen
- → Datei: `apps/web/next.config.ts`
- [ ] **Task 10:** Smoke-Test: App unter gehärteter CSP starten, Browser-DevTools auf CSP-Violations prüfen (mindestens Login → Dashboard → Timeline → Allocations).
Gefundene Violations entweder beheben (Move to external file / Nonce) oder als Known Exception dokumentieren.
---
### Issue #23 — Active-Session-Registry bei jedem Request prüfen
- [ ] **Task 11:** Im tRPC-Route-Handler (`apps/web/src/app/api/trpc/[trpc]/route.ts`) nach dem `auth()`-Call die `jti` aus dem Session-Token lesen und gegen `prisma.activeSession` validieren:
```ts
const jti = session?.user?.jti as string | undefined;
if (jti) {
const active = await prisma.activeSession.findUnique({ where: { jti } });
if (!active) {
return NextResponse.json({ error: "Session revoked" }, { status: 401 });
}
}
```
- Gilt für alle authentisierten tRPC-Requests
- Gilt auch für nicht-tRPC Auth-Pfade wenn vorhanden (Route-Handler prüfen)
- → Datei: `apps/web/src/app/api/trpc/[trpc]/route.ts`
- [ ] **Task 12:** Unit-/Integrations-Tests:
- gültige aktive Session → Request durch
- ausgeloggte (gelöschte) Session → 401
- durch Concurrent-Session-Limit verdrängte Session → 401
- → Datei: `apps/web/src/app/api/trpc/[trpc]/route.test.ts` (neu oder ergänzen)
---
### Issue #24 — Docker-Setup host-unabhängig
- [ ] **Task 13:** `Dockerfile.dev` prüfen und absichern:
- `pnpm install` muss im Container ablaufen (kein Volume-Mount der Host-`node_modules`)
- `prisma generate` muss Teil des Starts sein (nicht als Host-Voraussetzung)
- Fehlende Systempakete (z. B. OpenSSL für Prisma) explizit installieren
- → Datei: `Dockerfile.dev`
- [ ] **Task 14:** `Dockerfile.prod` prüfen:
- Multi-Stage-Build: Build-Stage hat pnpm + alle Dev-Deps; Runtime-Stage nur Prod-Artefakte
- Generierte Prisma-Artefakte (`node_modules/.prisma`) korrekt aus Build-Stage kopiert
- → Datei: `Dockerfile.prod`
- [ ] **Task 15:** `docker-compose.yml` absichern:
- `node_modules`-Volume-Override korrekt gesetzt damit Host-Modules nicht reinmappen
- `app-dev-start.sh` ausführbar und alle Schritte (generate, migrate, start) enthalten
- → Dateien: `docker-compose.yml`, `tooling/docker/app-dev-start.sh`
- [ ] **Task 16:** Frischer-Checkout-Smoke-Test dokumentieren:
```bash
git clone … && cd capakraken
docker compose up --build
# → App erreichbar, Login funktioniert, keine Host-Abhängigkeiten
```
→ Schritt in `docs/` oder `README.md` festhalten
---
## Abhängigkeiten
```
Task 1 → Task 2 (Tests setzen fertige Impl voraus)
Task 3 → Task 4 → Task 5 (Dep-Install → Impl → Test)
Task 6 → Task 7 → Task 8 (Guard-Lib → Integration → Tests)
Task 9 → Task 10 (CSP-Änderung → Smoke-Test)
Task 11 → Task 12 (Session-Impl → Tests)
Task 1316 unabhängig von allen anderen (Docker-only)
Parallel möglich (nach Task-Gruppe):
- #21 (Tasks 12) || #19 (Tasks 35) || #24 (Tasks 1316)
- #20 (Tasks 68) || #22 (Tasks 910) — erst nachdem #21/#19 fertig sind (separater Branch)
- #23 (Tasks 1112) — zuletzt, da Auth-Pfad berührt
```
---
## Akzeptanzkriterien
- [ ] `pnpm test:unit` läuft grün
- [ ] `pnpm --filter @capakraken/web exec tsc --noEmit` — keine neuen Errors
- [ ] `pnpm lint` — sauber
- [ ] **#21:** `/api/perf` ohne `CRON_SECRET` gibt 401/403, Query-Token wird nicht mehr akzeptiert
- [ ] **#19:** `MfaSetup` macht keinen Request an `qrserver.com` oder `chart.googleapis.com`
- [ ] **#20:** Webhook-Save und Dispatch für `http://127.0.0.1/…` gibt `BAD_REQUEST`
- [ ] **#22:** Browser-DevTools zeigt keine CSP-Violations für Haupt-User-Flows in Production-Mode
- [ ] **#23:** Request mit gelöschter `ActiveSession`-`jti` gibt 401
- [ ] **#24:** `docker compose up --build` auf sauberem Checkout bootet die App ohne Host-Abhängigkeiten
---
## Risiken & offene Fragen
| Risiko | Einschätzung | Maßnahme |
|--------|-------------|----------|
| CSP-Nonces in Next.js 15: `<Script>` und Tailwind CSS benötigen ggf. Nonces | Mittel | Vor Task 9 in Next.js-15-Doku recherchieren; ggf. nur `unsafe-eval` entfernen als erster Schritt |
| Session-Registry-Check (#23) erhöht DB-Load: jeder tRPC-Request = 1 DB-Read | Mittel | Redis-Cache mit kurzer TTL (30s) als Opt-in; erst messen ob nötig |
| SSRF-Guard DNS-Lookup: async, könnte Race-Condition durch DNS-Rebinding haben | Niedrig | Nach DNS-Lookup Socket-Verbindung ebenfalls gegen IP prüfen (defense-in-depth) |
| Docker #24: `node_modules`-Volume-Semantik bei pnpm-Workspaces komplex | Mittel | Symlink-Struktur von pnpm in Container testen; ggf. `--shamefully-hoist` Flag |
| `jti` im Session-Token: Auth.js-Version muss `jti` ins JWT schreiben | Offen | In `auth.ts` prüfen ob `token.jti` tatsächlich im JWT-Callback persistiert wird |