New users on a shared device were picking up a previous user's stale
(potentially empty) dashboard layout from localStorage because the key
"capakraken_dashboard_v1" was not user-scoped.
- useDashboardLayout: key is now capakraken_dashboard_v1_{userId};
userId is resolved via trpc.user.me before touching localStorage
- Initial state falls back to createDefaultDashboardLayout() until
userId resolves, then hydrates from the user-scoped key
- DB layout still wins over localStorage when it has data (unchanged)
- E2E test suite covers: new-user flow, modal widget list, add widget
persists after reload, cross-user localStorage isolation
- plan.md: added ticket #27 implementation plan
Co-Authored-By: claude-flow <ruv@ruv.net>
18 KiB
CapaKraken — Umsetzungsplan
Gitea-Repo: https://gitea.hartmut-noerenberg.com/Hartmut/plANARCHY
Stand: 2026-04-01 | Issues: #19–#27
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
- T-1:
useDashboardLayoutauf user-scoped localStorage umstellen →useDashboardLayout.ts - 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-reviewsetzen
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
mainmit 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 addaller 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:unitläuft grün (beide Pakete)tsc --noEmitohne Fehler- Alle neuen Dateien in einem sauberen Commit auf
main git statusdanach: 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:
REDIS_URLindocker-compose.ymlnutzt${REDIS_URL:-redis://redis:6379}— Host-Env-Var kann Docker-internen Wert überschreiben (gleiche Klasse wie das behobeneDATABASE_URL-Problem)- Migration-Strategy undokumentiert: DB per
db pushaufgebaut, dann Migration hinzugefügt →migrate deployscheitert mit P3005, erfordertemigrate resolve --applied - 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.mderstellen 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.listViewkönnte ein Pflicht-Input-Objekt erfordern → FallsBAD_REQUESTstatt FORBIDDEN, Schema im Router prüfen und minimalen Input übergeben- Viewer-Permissions aus DB-Seed (SystemRoleConfig) — prüfen ob VIEWER tatsächlich kein
VIEW_PLANNINGhat
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 inapps/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/qrcodezuapps/web/package.jsonhinzufügen,pnpm installausführen. -
Task 4:
MfaSetup.tsxumschreiben:<img src="https://api.qrserver.com/...">durch lokale QR-Generierung ersetzenqrcode.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>mitqrserver.comoderchart.googleapis.comgerendert wird - → Datei:
apps/web/src/components/security/MfaSetup.test.tsx(neu oder bestehend ergänzen)
- Unit-Test oder Playwright-Test der prüft, dass kein
Issue #20 — Webhook SSRF-Schutz
-
Task 6:
ssrf-guard.tserstellen mit einerassertWebhookUrlAllowed(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.16–31.x,192.168.x), Link-Local (169.254.x), Cloud-Metadata (169.254.169.254) - Blockt: alle Schemes außer
https(undhttpnur wenn expliziter Dev-Override gesetzt) - Wirft
TRPCError({ code: "BAD_REQUEST" })mit allgemeiner Fehlermeldung (ohne IP preiszugeben) - → Datei:
packages/api/src/lib/ssrf-guard.ts
- Parst die URL, löst Hostname auf (DNS-Check via Node
-
Task 7:
ssrf-guardinwebhook-support.tsundwebhook-dispatcher.tsintegrieren:- Vor Speicherung + vor Dispatch
assertWebhookUrlAllowed(url)aufrufen - → Dateien:
packages/api/src/router/webhook-support.ts,packages/api/src/lib/webhook-dispatcher.ts
- Vor Speicherung + vor Dispatch
-
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)
- Erlaubt:
Issue #22 — CSP härten
-
Task 9: CSP in
apps/web/next.config.tsüberarbeiten:unsafe-evalentfernen oder nur fürNODE_ENV === "development"erlaubenunsafe-inlineausscript-srcentfernen- 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 demauth()-Call diejtiaus dem Session-Token lesen und gegenprisma.activeSessionvalidieren: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.devprüfen und absichern:pnpm installmuss im Container ablaufen (kein Volume-Mount der Host-node_modules)prisma generatemuss Teil des Starts sein (nicht als Host-Voraussetzung)- Fehlende Systempakete (z. B. OpenSSL für Prisma) explizit installieren
- → Datei:
Dockerfile.dev
-
Task 14:
Dockerfile.prodprü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.ymlabsichern:node_modules-Volume-Override korrekt gesetzt damit Host-Modules nicht reinmappenapp-dev-start.shausfü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:
git clone … && cd capakraken docker compose up --build # → App erreichbar, Login funktioniert, keine Host-Abhängigkeiten→ Schritt in
docs/oderREADME.mdfesthalten
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 13–16 unabhängig von allen anderen (Docker-only)
Parallel möglich (nach Task-Gruppe):
- #21 (Tasks 1–2) || #19 (Tasks 3–5) || #24 (Tasks 13–16)
- #20 (Tasks 6–8) || #22 (Tasks 9–10) — erst nachdem #21/#19 fertig sind (separater Branch)
- #23 (Tasks 11–12) — zuletzt, da Auth-Pfad berührt
Akzeptanzkriterien
pnpm test:unitläuft grünpnpm --filter @capakraken/web exec tsc --noEmit— keine neuen Errorspnpm lint— sauber- #21:
/api/perfohneCRON_SECRETgibt 401/403, Query-Token wird nicht mehr akzeptiert - #19:
MfaSetupmacht keinen Request anqrserver.comoderchart.googleapis.com - #20: Webhook-Save und Dispatch für
http://127.0.0.1/…gibtBAD_REQUEST - #22: Browser-DevTools zeigt keine CSP-Violations für Haupt-User-Flows in Production-Mode
- #23: Request mit gelöschter
ActiveSession-jtigibt 401 - #24:
docker compose up --buildauf 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 |