--- phase: quick-260907-e8k plan: 01 subsystem: auth tags: [nextjs, app-router, server-components, next-intl, vitest, module-access] requires: - phase: 15-modul-berechtigungen-gruppen-user-grants provides: checkModuleAccess (module-access-actions.ts) — fails-closed session-cookie-based access check against GET /modules/active provides: - Shared ModuleAccessDenied 403 markup component (translation-free, props-driven) - Reusable ModuleAccessGate server component wrapping checkModuleAccess with a second fail-closed layer - layout.tsx access gate for each of the four module-owned route trees (cert-manager, dkv-fleet, domaincheck, tender-radar) - Generic [category]/[moduleSlug]/page.tsx route rewired onto the shared gate - module-layouts.test.tsx guarding against future module directories missing a layout affects: [15-modul-berechtigungen-gruppen-user-grants, module-routing, module-marketplace] actuals: tokens: 5709 tasks: 2 commits: 2 tech-stack: added: [] patterns: - "Server-side access gate as a reusable component (ModuleAccessGate) consumed both via App Router layout.tsx (covers whole route subtrees) and directly in a page — single source of truth for the 403 decision and markup" - "Fail-closed defense in depth: checkModuleAccess already fails closed; ModuleAccessGate adds a second try/catch layer so a future change to the check function cannot silently open the gate" key-files: created: - apps/web/src/components/modules/module-access-denied.tsx - apps/web/src/components/modules/module-access-gate.tsx - apps/web/src/components/modules/module-access-gate.test.tsx - apps/web/src/app/(portal)/modules/cert-manager/layout.tsx - apps/web/src/app/(portal)/modules/dkv-fleet/layout.tsx - apps/web/src/app/(portal)/modules/domaincheck/layout.tsx - apps/web/src/app/(portal)/modules/tender-radar/layout.tsx - apps/web/src/app/(portal)/modules/module-layouts.test.tsx modified: - apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx - apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-access.test.tsx key-decisions: - "403 markup and translation lookup live once in ModuleAccessGate/ModuleAccessDenied; the generic route and all four module-owned layouts render the same component instead of each having its own inline check" - "ModuleAccessDenied stays translation-free (props for title/body/backToDashboard) — only ModuleAccessGate calls getTranslations, keeping the presentational component trivially testable without mocking next-intl" - "A layout.tsx per module directory is used instead of per-page checks, because a Next.js App Router layout automatically wraps every nested subroute — my-sources/settings/vehicles close without any file of their own" patterns-established: - "Module directory coverage test: module-layouts.test.tsx reads the modules/ directory via node:fs at test time and requires a layout.tsx for every non-dynamic subdirectory, so a future module added without a gate fails CI instead of silently reopening the vulnerability" requirements-completed: [PERM-04] coverage: - id: D1 description: "ModuleAccessDenied — shared, translation-free 403 markup component" requirement: "PERM-04" verification: - kind: unit ref: "apps/web/src/components/modules/module-access-gate.test.tsx#renders the 403 heading, body, and dashboard link and not the children when access is denied" status: pass human_judgment: false - id: D2 description: "ModuleAccessGate — server-side access gate calling checkModuleAccess, fails closed on denial and on thrown exceptions, passes children through only on explicit grant" requirement: "PERM-04" verification: - kind: unit ref: "apps/web/src/components/modules/module-access-gate.test.tsx (4 cases: granted, denied, exception, slug pass-through)" status: pass human_judgment: false - id: D3 description: "layout.tsx for cert-manager, dkv-fleet, domaincheck, tender-radar — each wraps its whole route subtree (including nested routes like my-sources, settings, vehicles) in ModuleAccessGate with the correct slug" requirement: "PERM-04" verification: - kind: unit ref: "apps/web/src/app/(portal)/modules/module-layouts.test.tsx#module layouts — ModuleAccessGate slug wiring" status: pass - kind: unit ref: "apps/web/src/app/(portal)/modules/module-layouts.test.tsx#module directory coverage — every module has a layout" status: pass - kind: manual_procedural ref: "Browser-Gegenprobe (Abschnitt A/B/C in 260907-e8k-PLAN.md) — vom Orchestrator nach Docker-Rebuild auszufuehren" status: unknown human_judgment: true rationale: "Der eigentliche Behebungserfolg (403 als Serveranwort statt Modulseite, keine Ueberdeckung fuer berechtigte Nutzer/Admins) ist nur im echten Browser gegen die Docker-Instanz mit drei realen Konten pruefbar — WINDOWS #10 bleibt laut Plan explizit offen, bis diese Gegenprobe bestanden ist." - id: D4 description: "Generische Route [category]/[moduleSlug]/page.tsx auf ModuleAccessGate umgestellt, inline-403-Markup entfernt" requirement: "PERM-04" verification: - kind: unit ref: "apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-access.test.tsx#ExpandedModulePage — passes slug through to ModuleAccessGate" status: pass human_judgment: false duration: 25min completed: 2026-09-07 status: complete --- # Phase quick-260907-e8k Plan 01: Modul-Zugriffs-Guard auf Layout-Ebene Summary **Vier fest verdrahtete Modulrouten (cert-manager, dkv-fleet, domaincheck, tender-radar) liefen bislang komplett am 403-Guard vorbei — sie sind jetzt ueber ein layout.tsx pro Modulverzeichnis an dieselbe ModuleAccessGate-Komponente angeschlossen wie die generische Route.** ## Performance - **Duration:** 25 min - **Started:** 2026-09-07T08:03Z (ca.) - **Completed:** 2026-09-07T08:28Z - **Tasks:** 2/2 completed - **Files modified:** 10 (8 created, 2 modified) ## Accomplishments - Das bisher inline in `[category]/[moduleSlug]/page.tsx` stehende 403-Markup existiert jetzt genau einmal, in `ModuleAccessDenied` — eine reine, uebersetzungsfreie Praesentationskomponente. - `ModuleAccessGate` buendelt die Zugriffspruefung als wiederverwendbare Server Component: ruft `checkModuleAccess`, faengt zusaetzlich jede geworfene Ausnahme als "kein Zugriff" ab (zweite Verteidigungslinie ueber die bereits geschlossen ausfallende Funktion), gibt Kinder nur bei explizit gewaehrtem Zugriff durch. - Vier neue `layout.tsx`-Dateien (eine pro Modulverzeichnis) schliessen jetzt auch `tender-radar/my-sources`, `tender-radar/settings`, `dkv-fleet/settings` und `dkv-fleet/vehicles` ab — ohne eigene Datei je Unterroute, weil ein App-Router-Layout automatisch alle verschachtelten Unterrouten mit umschliesst. - Ein Struktur-Test (`module-layouts.test.tsx`) liest das Modulverzeichnis zur Testzeit per `node:fs` aus und verlangt fuer jedes nicht-dynamische Unterverzeichnis eine `layout.tsx` — ein kuenftig hinzugefuegtes Modul ohne Gate faellt damit in der Testsuite auf, nicht erst in Produktion. - Die generische Route ist auf dieselben Bausteine umgestellt; ihr eigener Test prueft jetzt die Weitergabe des Slugs statt die 403-Darstellung erneut zu pruefen (die liegt seit Task 1 vollstaendig in `module-access-gate.test.tsx`). ## Task Commits Each task was committed atomically: 1. **Task 1: Gemeinsame 403-Komponente und ModuleAccessGate mit Tests** - `4a23e13` (feat) 2. **Task 2: Vier Modul-Layouts, Strukturtest und Umstellung der generischen Route** - `69b6418` (feat) ## Files Created/Modified - `apps/web/src/components/modules/module-access-denied.tsx` - Uebersetzungsfreie 403-Praesentationskomponente (title/body/backToDashboard als Props) - `apps/web/src/components/modules/module-access-gate.tsx` - Server-Component-Gate: ruft checkModuleAccess, faengt Ausnahmen ab, rendert Kinder nur bei explizitem Zugriff - `apps/web/src/components/modules/module-access-gate.test.tsx` - 4 Testfaelle: durchgelassen, verweigert, Ausnahme, Slug-Weitergabe - `apps/web/src/app/(portal)/modules/cert-manager/layout.tsx` - Gate mit Slug "cert-manager" - `apps/web/src/app/(portal)/modules/dkv-fleet/layout.tsx` - Gate mit Slug "dkv-fleet" - `apps/web/src/app/(portal)/modules/domaincheck/layout.tsx` - Gate mit Slug "domaincheck" - `apps/web/src/app/(portal)/modules/tender-radar/layout.tsx` - Gate mit Slug "tender-radar" - `apps/web/src/app/(portal)/modules/module-layouts.test.tsx` - Slug-Verdrahtung je Layout + Verzeichnis-Abdeckungstest - `apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx` - Auf ModuleAccessGate umgestellt, Inline-403 und direkter checkModuleAccess-Aufruf entfernt - `apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-access.test.tsx` - Seiten-Testfaelle auf Gate-Weitergabe umgeschrieben, checkModuleAccess- und ModuleShell-Whitelist-Bloecke unveraendert ## Decisions Made - Keine neuen i18n-Schluessel: `modules.accessDenied.{title,body,backToDashboard}` existierten bereits in `de.json`/`en.json` und werden unveraendert weiterverwendet. - Layouts bleiben gewoehnliche synchrone Server Components ohne Client-Direktive, ohne Hooks — reiner Import + Return des Gates. ## Deviations from Plan None - plan executed exactly as written. ## Issues Encountered None. ## User Setup Required None - no external service configuration required. ## Next Phase Readiness Automatisierte Verifikation vollstaendig gruen (Details unten). Die Browser-Gegenprobe aus Abschnitt A/B/C des Plans (drei Konten, acht+ Adressen, Docker-Rebuild) ist laut Plan explizit Aufgabe des Orchestrators und wurde in dieser Ausfuehrung nicht durchgefuehrt — WINDOWS #10 bleibt bis dahin offen. ## Real verification results 1. `pnpm --filter @tessera/web exec vitest run src/components/modules/module-access-gate.test.tsx` — **4/4 passed** (Task 1 verify) 2. `pnpm --filter @tessera/web type-check` — clean, no output (Task 1 verify) 3. `pnpm --filter @tessera/web exec vitest run 'src/app/(portal)/modules/module-layouts.test.tsx' 'src/app/(portal)/modules/[category]/[moduleSlug]/module-access.test.tsx'` — **9/9 passed** (Task 2 verify) 4. `pnpm --filter @tessera/web exec vitest run` (full suite) — **213/213 passed, 36/36 test files** (Task 2 verify) 5. `pnpm --filter @tessera/web type-check` — clean, no output (Task 2 verify) 6. `pnpm --filter @tessera/web build` — **succeeded**, all 27 routes generated including the four module routes and the generic route; only pre-existing, unrelated warning (jose Edge Runtime CompressionStream/DecompressionStream, not touched by this plan) 7. Browser-Gegenprobe (human-check) — **not run**; per plan and per task instructions this is executed by the orchestrator after a Docker rebuild, not by the executor. --- *Phase: quick-260907-e8k* *Completed: 2026-09-07* ## Self-Check: PASSED All 10 created/modified source files and this SUMMARY.md verified present on disk. Both task commits (`4a23e13`, `69b6418`) verified present in `git log`.