diff --git a/.planning/quick/260907-e8k-zugriffs-guard-greift-nicht-auf-modul-ei/260907-e8k-PLAN.md b/.planning/quick/260907-e8k-zugriffs-guard-greift-nicht-auf-modul-ei/260907-e8k-PLAN.md new file mode 100644 index 0000000..36d2581 --- /dev/null +++ b/.planning/quick/260907-e8k-zugriffs-guard-greift-nicht-auf-modul-ei/260907-e8k-PLAN.md @@ -0,0 +1,360 @@ +--- +quick_id: 260907-e8k +slug: zugriffs-guard-greift-nicht-auf-modul-ei +date: 2026-09-07 +status: planned +relates_to: 15-modul-berechtigungen-gruppen-user-grants +windows_ref: 10 +severity: high + +phase: quick-260907-e8k +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - 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 + - apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx + - apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-access.test.tsx +autonomous: true +requirements: [PERM-04] + +estimate: + tokens: 45000 + raw_tokens: 45000 + tasks: 2 + confidence: low + +must_haves: + truths: + - "Ein USER ohne Modulfreigabe bekommt auf /modules/tender-radar, /modules/tender-radar/my-sources, /modules/tender-radar/settings, /modules/dkv-fleet, /modules/dkv-fleet/settings, /modules/dkv-fleet/vehicles, /modules/cert-manager und /modules/domaincheck die 403-Seite als Serverantwort statt der Modulseite." + - "Ein USER MIT Freigabe sowie jeder ADMIN/SUPER_ADMIN sieht genau dieselben Adressen unveraendert vollstaendig — die Sperre trifft niemanden zu viel." + - "Die 403-Darstellung ist die Serverantwort selbst: keine Weiterleitung, kein Not-Found (D-07). Der Nutzer erfaehrt, dass das Modul existiert und ihm die Freigabe fehlt." + - "Die Zugriffspruefung bleibt geschlossen bei fehlendem Sitzungscookie, nicht-ok-Antwort der API, Netzfehler UND geworfener Ausnahme." + - "Die 403-Markierung steht genau einmal im Code — generische Route und modul-eigene Routen rendern dieselbe Komponente." + artifacts: + - apps/web/src/components/modules/module-access-denied.tsx + - apps/web/src/components/modules/module-access-gate.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/components/modules/module-access-gate.test.tsx + - apps/web/src/app/(portal)/modules/module-layouts.test.tsx + key_links: + - "layout.tsx jedes Modulverzeichnisses -> ModuleAccessGate mit dem Slug, der exakt dem Verzeichnisnamen entspricht (cert-manager, dkv-fleet, domaincheck, tender-radar sind zugleich die DB-Slugs)" + - "ModuleAccessGate -> checkModuleAccess -> GET /modules/active — dieselbe Zugriffsaufloesung wie Sidebar und ModuleGuard (D-01), keine zweite Rollenlogik im Frontend" + - "Ein Layout deckt in Next.js App Router alle verschachtelten Unterrouten mit ab — genau darueber werden my-sources, settings und vehicles ohne eigene Datei geschlossen" +--- + + +Der serverseitige Zugriffs-Guard fuer Module greift heute nur auf der generischen +Route `modules/[category]/[moduleSlug]/page.tsx`. Die vier Module mit fest +verdrahteten eigenen Routen laufen daran vorbei und rendern fuer einen USER ohne +Freigabe die volle Seite (WINDOWS #10, gemessen am 2026-09-07). + +Dieser Plan zieht die Pruefung in ein wiederverwendbares Gate und haengt sie ueber +je ein `layout.tsx` vor jedes Modulverzeichnis. Ein Layout deckt alle +Unterrouten mit ab — damit schliessen sich `my-sources`, `settings` und +`vehicles` ohne eigene Datei. + +Purpose: PERM-04 gilt bisher nur fuer eine von zwei Routenformen. Es werden zwar +keine Daten preisgegeben (die API antwortet ueberall mit 403), aber der Nutzer +sieht bedienbare Knoepfe, die ins Leere laufen, und rohe englische Fehlertexte +mitten in der deutschen Oberflaeche. +Output: Eine gemeinsame 403-Komponente, ein Zugriffs-Gate als Server Component, +vier Layout-Dateien, die zugehoerigen Tests und die auf die gemeinsamen Bausteine +umgestellte generische Route. + + + +@~/.claude/gsd-core/workflows/execute-plan.md +@~/.claude/gsd-core/templates/summary.md + + + +@.planning/STATE.md +@CLAUDE.md +@.planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-UAT-2026-09-07.md + +@apps/web/src/lib/module-access-actions.ts +@apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx +@apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-access.test.tsx + + + + + + Task 1: Gemeinsame 403-Komponente und ModuleAccessGate mit Tests + +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/[category]/[moduleSlug]/page.tsx (das heute inline stehende 403-Markup, wird 1:1 uebernommen), +apps/web/src/lib/module-access-actions.ts (checkModuleAccess, faellt bereits geschlossen aus — T-15-29), +apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-access.test.tsx (Mock-Muster fuer next-intl/server und vi.doMock, wird uebernommen) + + + - Gate mit gewaehrter Freigabe: rendert die uebergebenen Kinder, und die 403-Ueberschrift kommt nicht vor. + - Gate mit verweigerter Freigabe: rendert Ueberschrift, Erlaeuterungstext und Rueckweg der 403-Seite, und die Kinder kommen NICHT vor. + - Gate, wenn die Zugriffspruefung eine Ausnahme wirft: rendert die 403-Seite, nicht die Kinder (geschlossen fallen). + - Gate uebergibt den Slug unveraendert an die Zugriffspruefung: bei Aufruf mit "tender-radar" wird die Pruefung genau einmal und genau mit "tender-radar" aufgerufen. + - Die erwarteten deutschen Texte stehen im Test handgeschrieben ("Kein Zugriff auf dieses Modul", "Zur Startseite") und werden NICHT ueber dieselbe Uebersetzungsfunktion gebildet, die der Produktivcode nutzt — der Uebersetzungszugriff wird wie in module-access.test.tsx mit einer handgepflegten Schluesseltabelle nachgebildet. + + +Erstelle `apps/web/src/components/modules/module-access-denied.tsx` mit der +Komponente `ModuleAccessDenied`. Sie ist bewusst synchron und uebersetzungsfrei: +sie nimmt die drei fertigen Texte als Eigenschaften `title`, `body` und +`backToDashboard` entgegen und rendert das 403-Markup, das heute inline in +`[category]/[moduleSlug]/page.tsx` steht — dieselbe Aussenhuelle, dasselbe +Schloss-Symbol, derselbe `Link` auf `/`. Kopiere das Markup unveraendert, damit +die bereits im Browser bestaetigte Ebene 2 der UAT optisch identisch bleibt. +Die Komponente traegt keine Client-Direktive am Dateikopf. + +Erstelle `apps/web/src/components/modules/module-access-gate.tsx` mit der +asynchronen Server-Komponente `ModuleAccessGate({ moduleSlug, children })`. Sie +ruft `checkModuleAccess(moduleSlug)` aus `@/lib/module-access-actions` auf. Bei +gewaehrtem Zugriff gibt sie die Kinder zurueck. Andernfalls holt sie die +Uebersetzungen ueber `getTranslations('modules')` aus `next-intl/server` und +rendert `ModuleAccessDenied` mit `accessDenied.title`, `accessDenied.body` und +`accessDenied.backToDashboard`. Es entstehen KEINE neuen i18n-Schluessel — die +drei existieren bereits in `src/messages/de.json` und `src/messages/en.json` +unter `modules.accessDenied`. + +Umschliesse den Aufruf der Zugriffspruefung mit einer Fehlerbehandlung, die jede +geworfene Ausnahme als "kein Zugriff" wertet. `checkModuleAccess` faengt heute +selbst ab und wirft nicht; die Absicherung im Gate ist die zweite Verteidigungslinie, +damit ein spaeterer Umbau der Pruefung das Gate nicht versehentlich oeffnet. Die +Bedingung ist ausdruecklich "nur bei explizit gewaehrtem Zugriff durchlassen", +niemals "nur bei explizit verweigertem Zugriff sperren". Keine Rollenabfrage im +Frontend: ADMIN und SUPER_ADMIN loesen ueber `GET /modules/active` ohnehin als +zugriffsberechtigt auf (D-01). Das Gate leitet nicht weiter und ruft kein +Not-Found auf — die 403-Darstellung ist die Serverantwort (D-07). Die Datei +traegt keine Client-Direktive am Dateikopf. + +Erstelle `apps/web/src/components/modules/module-access-gate.test.tsx` nach dem +Muster von `module-access.test.tsx`: `next-intl/server` per `vi.mock` mit einer +handgeschriebenen Schluesseltabelle, `@/lib/module-access-actions` per +`vi.doMock` je Testfall, `afterEach` mit `cleanup`, `vi.resetModules` und +`vi.doUnmock`. Da das Gate eine asynchrone Server-Komponente ist, wird es im Test +als Funktion aufgerufen und das Ergebnis abgewartet, bevor es an `render` +uebergeben wird — genau so, wie der bestehende Test die Seitenkomponente +behandelt. Decke die vier Faelle aus dem Verhaltensblock ab. + + + cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web exec vitest run src/components/modules/module-access-gate.test.tsx + cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web type-check + + +`module-access-gate.test.tsx` ist gruen mit mindestens vier Faellen (durchgelassen, +verweigert, Ausnahme, Slug-Weitergabe). `ModuleAccessDenied` enthaelt das +403-Markup, `ModuleAccessGate` enthaelt die Zugriffslogik, und die Typpruefung +des Web-Pakets ist fehlerfrei. + + + + + Task 2: Vier Modul-Layouts, Strukturtest und Umstellung der generischen Route + +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, +apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx, +apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-access.test.tsx + + +apps/web/src/components/modules/module-access-gate.tsx (aus Task 1), +apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-access.test.tsx (die beiden Testfaelle der Seitenkomponente werden umgeschrieben, die Bloecke zu checkModuleAccess und ModuleShell bleiben unangetastet) + + + - Jedes der vier Layouts gibt ein ModuleAccessGate zurueck, dessen Slug exakt dem Verzeichnisnamen entspricht: cert-manager, dkv-fleet, domaincheck, tender-radar. Die erwarteten Slugs stehen im Test handgeschrieben. + - Jedes Layout reicht die empfangenen Kinder unveraendert an das Gate weiter. + - Jedes Unterverzeichnis von modules/, dessen Name nicht mit einer eckigen Klammer beginnt, besitzt eine layout.tsx — ein kuenftig hinzugefuegtes Modulverzeichnis ohne Layout laesst den Test rot werden. + - Die generische Route uebergibt dem Gate den Slug aus den Routenparametern und als Kind die ModuleShell mit Kategorie und Slug. + + +Lege je eine `layout.tsx` in `apps/web/src/app/(portal)/modules/cert-manager/`, +`dkv-fleet/`, `domaincheck/` und `tender-radar/` an. Jede Datei exportiert eine +gewoehnliche, synchrone Standardfunktion, die `children` entgegennimmt und ein +`ModuleAccessGate` mit dem als Zeichenkette fest eingetragenen Slug des jeweiligen +Verzeichnisses zurueckgibt. Die Verzeichnisnamen sind zugleich die Modul-Slugs in +der Datenbank — nicht umbenennen, nicht ableiten, nicht aus dem Pfad berechnen. +Die Layouts bleiben Server Components: keine Client-Direktive am Dateikopf, keine +Hooks, keine Ereignisbehandler. Die Datei enthaelt ausser dem Import des Gates und +der Funktion nichts weiter; alle bestehenden Seiten bleiben unveraendert. + +Der entscheidende Effekt: ein Layout im App Router umschliesst automatisch alle +verschachtelten Unterrouten. `tender-radar/layout.tsx` schliesst damit auch +`tender-radar/my-sources` und `tender-radar/settings`, `dkv-fleet/layout.tsx` +auch `dkv-fleet/settings` und `dkv-fleet/vehicles`. Es werden keine weiteren +Layout-Dateien in den Unterverzeichnissen angelegt. + +Erstelle `apps/web/src/app/(portal)/modules/module-layouts.test.tsx`. Der Test +importiert die vier Layouts relativ, ruft jedes als Funktion mit einem +Platzhalter-Kind auf und prueft am zurueckgegebenen Element, dass die Eigenschaft +`moduleSlug` dem handgeschriebenen Erwartungswert entspricht und die Kinder +durchgereicht werden. Ergaenze einen zweiten Fall, der das Modulverzeichnis mit +`node:fs` ausliest (Pfad ueber `import.meta.url`, die Testdatei liegt selbst im +Modulverzeichnis), alle Unterverzeichnisse einsammelt, deren Name nicht mit einer +eckigen Klammer beginnt, und fuer jedes das Vorhandensein einer `layout.tsx` +verlangt. Damit faellt ein kuenftig hinzugefuegtes Modulverzeichnis ohne Gate auf. + +Stelle `[category]/[moduleSlug]/page.tsx` auf die gemeinsamen Bausteine um: die +Seite liest weiterhin die Routenparameter und gibt danach ein `ModuleAccessGate` +mit dem Slug zurueck, in dem die `ModuleShell` als Kind steht. Das bisher inline +stehende 403-Markup und der direkte Aufruf der Zugriffspruefung entfallen dort +ersatzlos, ebenso die dann ungenutzten Importe. Der erklaerende Kommentarkopf der +Datei wird auf den neuen Aufbau angepasst und behaelt den Hinweis auf D-07 und +die gemeinsame Zugriffsaufloesung. + +Passe `module-access.test.tsx` an: die beiden Faelle der Seitenkomponente pruefen +jetzt die Weitergabe statt der Darstellung — die Seite gibt ein Element zurueck, +dessen `moduleSlug` dem Slug aus den Routenparametern entspricht und dessen Kind +die ModuleShell mit derselben Kategorie und demselben Slug ist. Die Faelle zu +`checkModuleAccess` (faellt geschlossen) und zur ModuleShell-Whitelist bleiben +unveraendert erhalten; das Verhalten von 403 gegenueber Durchlassen ist seit +Task 1 im Gate-Test abgedeckt und wird hier nicht doppelt geprueft. + + + cd /home/vicolab/projects/tessera-ctl && 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 + cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web exec vitest run + cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web type-check + cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web build + Browser-Gegenprobe nach Neubau des Web-Containers — die vollstaendige Adress- und Kontenmatrix steht unten im Abschnitt "Browser-Gegenprobe". Wird vom Orchestrator ausgefuehrt, nicht vom Ausfuehrenden. + + +Die vier Layout-Dateien existieren, tragen jeweils den korrekten Slug und keine +Client-Direktive. `module-layouts.test.tsx` und `module-access.test.tsx` sind +gruen, die vollstaendige Web-Testsuite ist gruen, Typpruefung und +Produktionsbau des Web-Pakets laufen fehlerfrei durch. Das 403-Markup steht nur +noch in `module-access-denied.tsx`. + + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Browser -> Next.js Server Component | Der Nutzer bestimmt die Adresse frei. Sidebar und Marktplatz sind Bequemlichkeit, keine Zugriffskontrolle — ein Lesezeichen oder eine getippte Adresse umgeht beide. | +| Next.js Server Component -> NestJS API | Das Gate leitet das Sitzungscookie an `GET /modules/active` weiter; die verbindliche Entscheidung faellt in der API (ModuleAccessService), nicht im Frontend. | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-e8k-01 | Elevation of Privilege | modul-eigene Routen unter `(portal)/modules/{cert-manager,dkv-fleet,domaincheck,tender-radar}` samt Unterseiten | high | mitigate | Je ein `layout.tsx` pro Modulverzeichnis ruft `ModuleAccessGate`; das Layout umschliesst alle verschachtelten Unterrouten automatisch (Task 2) | +| T-e8k-02 | Information Disclosure | `ModuleAccessGate` bei Netzfehler oder geworfener Ausnahme der Zugriffspruefung | medium | mitigate | Fehlerbehandlung im Gate wertet jede Ausnahme als "kein Zugriff"; durchgelassen wird nur bei explizit gewaehrtem Zugriff (Task 1, Testfall 3) | +| T-e8k-03 | Tampering | Falscher Slug in einem der vier Layouts (Kopierfehler zwischen fast identischen Dateien) | medium | mitigate | `module-layouts.test.tsx` prueft je Verzeichnis den handgeschriebenen Erwartungs-Slug (Task 2) | +| T-e8k-04 | Elevation of Privilege | Kuenftig hinzugefuegtes Modulverzeichnis ohne Layout laeuft erneut am Gate vorbei | medium | mitigate | `module-layouts.test.tsx` liest das Modulverzeichnis aus und verlangt fuer jedes nicht-dynamische Unterverzeichnis eine `layout.tsx` (Task 2) | +| T-e8k-05 | Denial of Service | Ueberschiessende Sperre trifft ADMIN/SUPER_ADMIN oder freigegebene USER | medium | mitigate | Keine Rollenlogik im Frontend; das Gate fragt ausschliesslich `checkModuleAccess` (D-01). Browser-Gegenprobe deckt `admin` und `nutzer1` ausdruecklich ab | + +Keine Paketinstallationen in diesem Plan — das Package-Legitimacy-Gate entfaellt. + + + + +## Automatisiert + +1. `pnpm --filter @tessera/web exec vitest run` — vollstaendige Web-Suite gruen, + inklusive der neuen Dateien `module-access-gate.test.tsx` und + `module-layouts.test.tsx`. +2. `pnpm --filter @tessera/web type-check` — fehlerfrei. +3. `pnpm --filter @tessera/web build` — der Produktionsbau muss durchlaufen. Das + ist zugleich die Probe darauf, dass die Layouts echte Server Components + geblieben sind: eine Client-Direktive wuerde am Import der Server-Aktion + scheitern. + +## Browser-Gegenprobe (human-check, vom Orchestrator auszufuehren) + +Die Weboberflaeche laeuft aus einem gebauten Docker-Image. Vor jeder Pruefung +neu bauen und ersetzen, ein blosses Hochfahren baut nicht neu: + + docker compose build web && docker compose up -d --force-recreate web + +Danach `http://localhost:3000` im echten Browser. Zwischen den Konten jeweils +sauber abmelden. **Jede Adresse direkt eintippen**, nicht ueber die Sidebar +navigieren — geprueft wird genau der Weg an der Sidebar vorbei. Bei jedem +Adresswechsel innerhalb desselben Moduls einmal neu laden. + +**A — `nutzer2` / `Test1234!` (USER, KEINE Freigabe fuer tender-radar, HAT +cert-manager ueber die Gruppe "Alle Benutzer")** + +| Adresse | Erwartung | +|---|---| +| `/modules/procurement/tender-radar` | 403-Seite "Kein Zugriff auf dieses Modul" (unveraendert gegenueber heute) | +| `/modules/tender-radar` | 403-Seite statt Trefferliste — der eigentliche Befund | +| `/modules/tender-radar/my-sources` | 403-Seite statt Postfach- und Feed-Formular | +| `/modules/tender-radar/settings` | 403-Seite | +| `/modules/dkv-fleet` | 403-Seite, insbesondere ohne den Knopf "Jetzt pruefen" | +| `/modules/dkv-fleet/settings` | 403-Seite | +| `/modules/dkv-fleet/vehicles` | 403-Seite | +| `/modules/cert-manager` | **volle Seite** — hier besteht eine Freigabe, die Sperre darf nicht ueberschiessen | +| `/modules/domaincheck` | Ergebnis muss zu dem passen, was die Freigaben-Matrix in der Administration fuer `nutzer2` und domaincheck ausweist: mit Freigabe volle Seite, ohne Freigabe die 403-Seite | + +Zusaetzlich auf allen 403-Seiten pruefen: die Adresszeile bleibt unveraendert +stehen (keine Weiterleitung, D-07), die Seite ist deutsch, und es erscheinen +keine rohen englischen Techniktexte. + +**B — `nutzer1` / `Test1234!` (USER, HAT tender-radar ueber Gruppe und Direkt-Grant)** + +| Adresse | Erwartung | +|---|---| +| `/modules/tender-radar` | volle Seite mit Trefferliste | +| `/modules/tender-radar/my-sources` | volle Seite mit Postfach- und Feed-Formular | +| `/modules/tender-radar/settings` | Seite laedt (keine 403-Seite); die bereits bestehende Rollenunterscheidung innerhalb der Seite bleibt davon unberuehrt | + +**C — `admin` / `admin123` (SUPER_ADMIN)** + +| Adresse | Erwartung | +|---|---| +| `/modules/tender-radar` | volle Seite | +| `/modules/tender-radar/my-sources` | volle Seite | +| `/modules/dkv-fleet` | volle Seite | +| `/modules/cert-manager` | volle Seite | +| `/modules/domaincheck` | volle Seite | + +Faellt hier auch nur eine Seite auf 403, ist die Aenderung zu scharf und muss +zurueck — SUPER_ADMIN umgeht Grants (D-01). + +## Ledger + +`WINDOWS.md` #10 bleibt offen, bis Abschnitt A, B und C im Browser bestanden +sind. Erst dann auf geschlossen setzen, mit Datum und Belegbild. + + + + + +- Ein USER ohne Freigabe erhaelt auf allen acht in Abschnitt A gelisteten + Adressen die 403-Seite als Serverantwort, nicht die Modulseite. +- Ein USER mit Freigabe und jeder SUPER_ADMIN erreichen dieselben Adressen + unveraendert vollstaendig. +- Adresszeile bleibt bei der 403-Seite stehen: keine Weiterleitung, kein + Not-Found (D-07). +- Die Zugriffspruefung faellt weiterhin geschlossen aus — fehlendes Cookie, + nicht-ok-Antwort, Netzfehler und geworfene Ausnahme fuehren alle zur 403-Seite. +- Das 403-Markup existiert genau einmal im Code; die generische Route und die + vier modul-eigenen Routen rendern dieselbe Komponente. +- Vollstaendige Web-Testsuite, Typpruefung und Produktionsbau sind gruen. +- Ein neu angelegtes Modulverzeichnis ohne `layout.tsx` laesst + `module-layouts.test.tsx` fehlschlagen. + + + + +Erstelle `.planning/quick/260907-e8k-zugriffs-guard-greift-nicht-auf-modul-ei/260907-e8k-SUMMARY.md`, wenn fertig. +