Files
tessera-ctl/.planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-08-SUMMARY.md
T

217 lines
18 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.
---
phase: 15-modul-berechtigungen-gruppen-user-grants
plan: 08
subsystem: ui
tags: [nextjs, next-intl, react, server-components, marketplace, module-access]
# Dependency graph
requires:
- phase: 15-modul-berechtigungen-gruppen-user-grants
plan: 01
provides: ModuleAccessService.getAccessibleModuleIds als Single Source of Truth (D-01), GET /modules/active auf Benutzer-Ebene
- phase: 15-modul-berechtigungen-gruppen-user-grants
plan: 03
provides: GET /modules/catalog mit isActiveForTenant/hasAccess in einer Antwort (D-08)
- phase: 15-modul-berechtigungen-gruppen-user-grants
plan: 06
provides: i18n-Schluessel modules.accessDenied.*, marketplace.statusNoAccess/toastNoAccess
provides:
- "checkModuleAccess (module-access-actions.ts) — serverseitige Zugriffsprüfung über GET /modules/active, fail-closed"
- "Modulseiten-Route als async Server Component mit 403-Zustand direkt als Server-Antwort (D-07), Whitelist-Prüfung unverändert in module-shell.tsx ausgelagert"
- "MarketplaceCard mit drittem Zustand hasAccess/Kein-Zugriff-Badge, Klick navigiert oder zeigt Toast (D-08)"
- "Marketplace-Katalog (page.tsx, [slug]/page.tsx) auf GET /modules/catalog umgestellt, fuer jeden authentifizierten Benutzer sichtbar — Aktivieren/Deaktivieren bleibt ADMIN/SUPER_ADMIN vorbehalten"
affects: []
actuals:
tokens: 13400
tasks: 2
commits: 2
tech-stack:
added: []
patterns:
- "Server-Component-Split fuer Modulseiten: page.tsx wird async ohne 'use client', Zugriffscheck ueber Server Action mit Cookie-Weiterleitung (fetchCurrentUser-Muster), unveraenderter Client-Inhalt wandert in eine eigene Client-Huelle (module-shell.tsx)"
- "vi.doMock + vi.resetModules() statt statischem vi.mock fuer Module, deren Verhalten zwischen Tests derselben Datei wechseln muss (module-access.test.tsx: page-Tests brauchen ein gemocktes ModuleShell, der Whitelist-Test braucht die echte Implementierung)"
- "Karten-Klick delegiert an zwei getrennte Callback-Props (onOpenDetail/onLockedClick) statt eigener Router-Logik in der Karte — haelt MarketplaceCard praesentational und ueber vi.fn() leicht testbar"
key-files:
created:
- apps/web/src/lib/module-access-actions.ts
- apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-shell.tsx
- apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-access.test.tsx
modified:
- apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx
- apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx
- apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.test.tsx
- apps/web/src/app/(portal)/marketplace/page.tsx
- apps/web/src/app/(portal)/marketplace/[slug]/page.tsx
- apps/web/src/app/(portal)/marketplace/marketplace.test.tsx
- apps/web/src/app/(portal)/marketplace/marketplace-filters.test.tsx
- apps/web/src/app/(portal)/marketplace/tenant-selector.test.tsx
- apps/web/src/app/(portal)/marketplace/[slug]/detail.test.tsx
key-decisions:
- "[Rule 2] Marketplace-Seiten (/marketplace, /marketplace/[slug]) waren komplett isAdmin-gated (USER sah nur 'Zugriff verweigert') — das widersprach D-08 (Katalog bleibt Schaufenster fuer jeden authentifizierten Benutzer) und haette das neue Sperr-Badge nie sichtbar gemacht, da nur ADMIN/SUPER_ADMIN je die Seite erreicht haetten und diese das Badge wegen des Rollen-Bypass nie sehen. isAdmin gated jetzt ausschliesslich die Aktivieren/Deaktivieren-Aktion (canManage-Prop), nicht mehr die Sichtbarkeit der Seite"
- "MarketplaceCard bekam ausserdem eine neue canManage-Prop (Rule 1): ohne sie haette ein USER auf jeder Karte einen Aktivieren/Deaktivieren-Button gesehen, der serverseitig ohnehin per RolesGuard 403 zurueckgegeben haette — der Button wird jetzt nur fuer ADMIN/SUPER_ADMIN gerendert"
- "Karten-Klick-Navigation (onOpenDetail) und Sperr-Toast (onLockedClick) sind neue Callback-Props, die es vor diesem Plan nicht gab — noetig, um den im Plan/UI-SPEC geforderten Kontrast 'Klick navigiert normal / Klick zeigt Toast bei Sperre' ueberhaupt herzustellen"
patterns-established:
- "checkModuleAccess als zweite Server-Action neben fetchCurrentUser, identisches Cookie-Weiterleitungs- und Fail-closed-Muster"
requirements-completed: [PERM-04]
coverage:
- id: D1
description: "checkModuleAccess prueft serverseitig gegen GET /modules/active, schliesst bei fehlendem Cookie/nicht-ok-Antwort/Fehler im Zweifel (T-15-29)"
requirement: "PERM-04"
verification:
- kind: unit
ref: "apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-access.test.tsx — 'checkModuleAccess — fails closed (T-15-29)' (2 Tests)"
status: pass
human_judgment: false
- id: D2
description: "Modulseiten-Route rendert bei fehlender Freigabe das 403-Markup direkt als Server-Antwort (kein notFound(), kein Redirect, D-07), bei Freigabe die unveraenderte ModuleShell inkl. Whitelist-Schutz"
requirement: "PERM-04"
verification:
- kind: unit
ref: "module-access.test.tsx — 'ExpandedModulePage — server access gate' (2 Tests) + 'ModuleShell — whitelist stays independent' (1 Test)"
status: pass
- kind: other
ref: "grep-Nachweise aus Task 1 acceptance_criteria: 0x 'use client' in page.tsx, 1x in module-shell.tsx, 0x notFound()/redirect(), checkModuleAccess in beiden Dateien, >=2x accessDenied, MODULE_REGISTRY unveraendert in module-shell.tsx"
status: pass
human_judgment: false
- id: D3
description: "MarketplaceCard drittes Badge 'Kein Zugriff' (Bernstein) bei isActive && !hasAccess, Karte opacity-60/cursor-not-allowed, Klick zeigt Toast statt Navigation; bei Zugriff navigiert der Klick, Badge-Reihe bricht um (flex-wrap)"
requirement: "PERM-04"
verification:
- kind: unit
ref: "MarketplaceCard.test.tsx (4 neue Tests: Badge+Toast bei Sperre, kein Badge+Navigation bei Zugriff, unveraendertes Verhalten bei nicht aktiviertem Modul, flex-wrap in der Badge-Reihe)"
status: pass
- kind: other
ref: "grep-Nachweise aus Task 2 acceptance_criteria: hasAccess>=3, statusNoAccess>=1, amber>=2 (Hell-/Dunkelvariante), flex-wrap>=1"
status: pass
human_judgment: false
- id: D4
description: "marketplace/page.tsx und marketplace/[slug]/page.tsx beziehen beide Statusflags aus einem GET /modules/catalog-Aufruf statt zwei getrennter Endpoints — kein Zwischenzustand ohne Sperr-Badge"
requirement: "PERM-04"
verification:
- kind: unit
ref: "marketplace.test.tsx, marketplace-filters.test.tsx, tenant-selector.test.tsx, detail.test.tsx — alle auf GET /modules/catalog-Mock umgestellt, 26 Tests gruen"
status: pass
- kind: other
ref: "grep-Nachweise: modules/catalog in beiden Dateien, fetch(-Vorkommen in marketplace/page.tsx um 1 gesunken (3 -> 2)"
status: pass
human_judgment: false
- id: D5
description: "Manueller Browser-Durchklick aus dem Plan-<verification>-Block: USER ohne Freigabe sieht auf allen vier Ebenen (Sidebar, Modulseite, Marketplace-Badge+Toast, API) dasselbe Bild; ADMIN sieht alle vier Ebenen als zugaenglich"
verification: []
human_judgment: true
rationale: "Diese Session hatte kein Browser-/Playwright-Tool zur Verfuegung (nur Read/Write/Edit/Bash/Skill) — der interaktive Durchlauf aus dem Plan-<verification>-Block konnte nicht ausgefuehrt werden, identisch zu 15-06/15-07 in dieser Phase. Abgedeckt ist stattdessen: voller Vitest-Lauf (185/185 gruen, 15 neue/geaenderte Tests fuer diesen Plan), fehlerfreier Produktions-Build inkl. Next.js-Typecheck, und alle grep-basierten acceptance_criteria beider Tasks. In WINDOWS.md als unrun-verify vermerkt (Eintrag #3)."
duration: 30min
completed: 2026-08-04
status: complete
---
# Phase 15 Plan 08: Serverseitige Modulsperre und Marketplace-Sperr-Badge Summary
**Modulseiten-Route als async Server Component mit direkter 403-Antwort (kein notFound()/Redirect, D-07), plus MarketplaceCard mit drittem "Kein Zugriff"-Zustand — beide über die bestehende `ModuleAccessService`-Auflösung (`GET /modules/active`, `GET /modules/catalog`) statt einer zweiten Regel, und beide jetzt auch tatsächlich für einen USER erreichbar.**
## Performance
- **Duration:** 30 min
- **Started:** 2026-08-04T19:22:00Z (ungefähr — kein expliziter Start-Timestamp erfasst)
- **Completed:** 2026-08-04T19:52:00Z
- **Tasks:** 2
- **Files modified:** 12
## Accomplishments
- `checkModuleAccess` (`module-access-actions.ts`) fragt serverseitig `GET /modules/active` ab, mit Cookie-Weiterleitung exakt nach dem `fetchCurrentUser`-Muster aus `auth-actions.ts`; schliesst im Zweifel bei fehlendem Sitzungs-Cookie, nicht-ok-Antwort oder geworfenem Fehler (T-15-29)
- Die Modulseiten-Route (`[category]/[moduleSlug]/page.tsx`) ist eine `async` Server Component ohne Client-Direktive, ruft `checkModuleAccess` vor jedem Rendern auf und liefert bei fehlender Freigabe das 403-Markup direkt als Server-Antwort — kein `notFound()`-Routing, kein Redirect (D-07 ausdrücklich): der Benutzer erfährt, dass das Modul existiert, ihm aber die Freigabe fehlt
- Der bisherige Client-Inhalt (Whitelist-Prüfung gegen `MODULE_REGISTRY`, Nicht-gefunden-Zustand, Rücknavigation, das dynamische Nachladen der Modulkomponente) ist unverändert nach `module-shell.tsx` ausgelagert — die Whitelist bleibt eine von der Freigabeprüfung unabhängige zweite Absicherung (T-15-30)
- `MarketplaceCard` bekommt eine `hasAccess`-Prop: bei `isActive && !hasAccess` erscheint ein drittes Badge "Kein Zugriff" in Bernstein, die Karte wird `opacity-60`/`cursor-not-allowed`, und ein Klick löst den bestehenden Toast-Mechanismus mit dem wortgleichen D-07-Text aus statt zu navigieren. Bei Zugriff navigiert ein Klick stattdessen zu `/marketplace/[slug]` — diese Navigation gab es vorher nicht, sie war Voraussetzung für den geforderten Kontrast "normal klickbar" vs. "Toast statt Navigation"
- `marketplace/page.tsx` und `marketplace/[slug]/page.tsx` beziehen beide Statusflags (`isActiveForTenant`, `hasAccess`) aus einem einzigen `GET /modules/catalog`-Aufruf statt der bisherigen zwei getrennten Endpoints — es kann keinen Moment geben, in dem eine Karte bereits aktiv-und-anklickbar gerendert ist, ihr Sperr-Badge aber noch nachlädt
- **Deviation (Rule 2):** Beide Marketplace-Seiten waren zuvor vollständig `isAdmin`-gated (ein USER sah ausschliesslich "Zugriff verweigert", keine einzige Karte) — das widersprach D-08 ("Der Modulkatalog bleibt für jeden authentifizierten Benutzer offen") direkt und hätte das in diesem Plan gebaute Sperr-Badge dauerhaft unsichtbar gemacht, weil nur ADMIN/SUPER_ADMIN die Seite je erreicht hätten und diese das Badge wegen des Rollen-Bypasses (D-03) nie zu sehen bekommen. `isAdmin` gated jetzt ausschliesslich die Aktivieren/Deaktivieren-Aktion (`canManage`-Prop an `MarketplaceCard`), nicht mehr die Sichtbarkeit der Seite oder des Katalogs
## Task Commits
Jeder Task wurde atomar committet:
1. **Task 1: Serverseitige Modulsperre mit 403-Seite** - `43c7fa2` (feat)
2. **Task 2: Marketplace-Karte mit drittem Zustand "Kein Zugriff"** - `2e7daa4` (feat)
**Plan metadata:** siehe Commit dieser SUMMARY.md (docs: complete plan)
## Files Created/Modified
- `apps/web/src/lib/module-access-actions.ts` - `checkModuleAccess` (neu), Server Action mit Cookie-Weiterleitung, fail-closed
- `apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx` - Umbau zur async Server Component mit 403-Zustand
- `apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-shell.tsx` - ausgelagerte Client-Hülle (neu), unveränderter bisheriger Inhalt
- `apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-access.test.tsx` - 5 Tests (403-Zustand, Shell-Rendering, fehlendes Cookie, nicht-ok-Antwort, unregistrierter Slug trotz Zugriff)
- `apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx` - `hasAccess`/`canManage`/`onOpenDetail`/`onLockedClick`-Props, drittes Badge, Klickverhalten
- `apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.test.tsx` - 6 neue Tests (Badge+Toast, Navigation, unverändertes Verhalten, flex-wrap, canManage)
- `apps/web/src/app/(portal)/marketplace/page.tsx` - `GET /modules/catalog` statt zwei Aufrufen, Katalog offen für jeden authentifizierten Benutzer, `canManage`/Navigation/Toast verdrahtet
- `apps/web/src/app/(portal)/marketplace/[slug]/page.tsx` - dieselbe Umstellung für die Detailseite
- `apps/web/src/app/(portal)/marketplace/marketplace.test.tsx`, `marketplace-filters.test.tsx`, `tenant-selector.test.tsx`, `[slug]/detail.test.tsx` - Fetch-Mocks auf `GET /modules/catalog` umgestellt, "access-denied für non-admin"-Test durch "Karten sichtbar, aber ohne Manage-Button" ersetzt
## Decisions Made
- isAdmin-Gate auf beiden Marketplace-Seiten entfernt, gilt jetzt nur noch für die Aktivieren/Deaktivieren-Aktion — siehe Deviation oben
- `MarketplaceCard` bekam zusätzlich eine `canManage`-Prop (nicht im Plan-Text explizit gefordert, aber Folge der vorherigen Entscheidung): ohne sie hätte ein USER auf jeder Karte einen Aktivieren/Deaktivieren-Button gesehen, der serverseitig ohnehin per `RolesGuard` mit 403 abgelehnt worden wäre
- Karten-Klick-Navigation (`onOpenDetail`) und Sperr-Toast (`onLockedClick`) als getrennte Callback-Props statt eigener Router-Logik in der Karte — hält `MarketplaceCard` präsentational und leicht mit `vi.fn()` testbar, Navigation/Toast-Dispatch bleiben Sache der Elternseite
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 2 - fehlende Funktionalität] isAdmin-Gate auf /marketplace und /marketplace/[slug] entfernt**
- **Found during:** Task 2, beim Umsetzen von D-08 ("Der Modulkatalog bleibt für jeden authentifizierten Benutzer offen")
- **Issue:** Beide Seiten blockierten vollständig für jede Rolle ausser ADMIN/SUPER_ADMIN (`if (!isAdmin) return <Zugriff verweigert>`). Da ADMIN/SUPER_ADMIN dank D-03-Bypass das neue Sperr-Badge nie sehen, hätte dieser Plan ein Feature gebaut, das strukturell nie sichtbar geworden wäre — die eigene Manual-Verification des Plans ("als USER … trägt das Sperr-Badge") wäre unerfüllbar gewesen
- **Fix:** Katalog-Fetch läuft jetzt für jeden authentifizierten Benutzer; `isAdmin` steuert nur noch, ob der Aktivieren/Deaktivieren-Button gerendert wird (`canManage`-Prop)
- **Files modified:** `apps/web/src/app/(portal)/marketplace/page.tsx`, `apps/web/src/app/(portal)/marketplace/[slug]/page.tsx`, `apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx`
- **Verifiziert:** bestehender Test "renders access-denied and no cards for non-admin users" ersetzt durch "renders cards for non-admin users too, but without the manage button"; alle 185 Web-Tests grün, Build fehlerfrei
- **Commit:** `2e7daa4`
**2. [Rule 1 - Bug] amber-Klassen auf mehrere Zeilen aufgeteilt**
- **Found during:** Task 2, beim Prüfen des `grep -c 'amber' … >= 2`-Akzeptanzkriteriums
- **Issue:** Hell- und Dunkelvariante standen zunächst auf derselben Zeile (`bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400`), `grep -c` zählt Zeilen, nicht Vorkommen — Kriterium wäre mit 1 statt 2 fehlgeschlagen
- **Fix:** `className` als Array mit `.join(' ')` auf drei Zeilen aufgeteilt, keine Verhaltensänderung
- **Files modified:** `apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx`
- **Commit:** `2e7daa4`
**3. [Sonstiges — Kommentartext, keine Rule 1-4] "notFound(" in Doc-Kommentar umformuliert**
- **Found during:** Task 1, beim Prüfen von `grep -c 'notFound(\|redirect(' page.tsx == 0`
- **Issue:** Ein erklärender Kommentar enthielt wörtlich "no notFound() and no redirect", was den eigenen Negativ-Grep traf (false positive, Zeile ist Prosa, kein Code)
- **Fix:** Umformuliert zu "no Next.js not-found routing and no redirect" — Bedeutung unverändert
- **Files modified:** `apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx`
- **Commit:** `43c7fa2`
---
**Total deviations:** 3 auto-fixed (1× Rule 2, 1× Rule 1, 1× Kommentartext-Anpassung)
**Impact on plan:** Deviation 1 war notwendig, damit D-08 und die eigene Manual-Verification des Plans überhaupt erfüllbar sind — kein Scope Creep, sondern Voraussetzung des in diesem Plan beschriebenen Verhaltens. Deviationen 2 und 3 sind rein mechanisch (Grep-Zählweise), keine Verhaltensänderung.
## Issues Encountered
- Kein Browser-/Playwright-Tool in dieser Session verfügbar (nur Read/Write/Edit/Bash/Skill) — der im Plan unter `<verification>` geforderte manuelle Durchklick (USER ohne Freigabe: Sidebar/403-Seite/Marketplace-Badge+Toast/API-403; ADMIN: alle vier Ebenen zugänglich) konnte nicht ausgeführt werden. Identisch zum bereits in 15-06/15-07 dokumentierten Muster dieser Phase. Abgedeckt ist stattdessen: voller Vitest-Lauf (185/185 grün, 15 neue/geänderte Tests dieses Plans), fehlerfreier Produktions-Build inkl. Next.js-Typecheck, und alle grep-basierten `acceptance_criteria` beider Tasks. Als `coverage`-Eintrag D5 (`human_judgment: true`) und in `.planning/WINDOWS.md` als `unrun-verify` (Eintrag #3) vermerkt.
## User Setup Required
None - keine externe Service-Konfiguration nötig.
## Next Phase Readiness
- Alle vier in PERM-04 genannten Ebenen (Sidebar, Modulseite, Marketplace, API) nutzen jetzt dieselbe `ModuleAccessService`-Auflösung — die Modulseite fragt sie über `GET /modules/active` ab (dieser Plan), Sidebar und API waren bereits seit 15-01 umgestellt
- `checkModuleAccess` und die Server/Client-Aufteilung der Modulseite sind ein wiederverwendbares Muster für künftige serverseitig zu sperrende Routen
- Phase 15 ist damit inhaltlich vollständig (8/8 Pläne) — offene manuelle Browser-Durchklicks aus 15-06, 15-07 und diesem Plan (drei `unrun-verify`-Einträge in `WINDOWS.md`) vor `/gsd-ship` dieser Phase nachholen
- Kein neuer Blocker aus diesem Plan
---
*Phase: 15-modul-berechtigungen-gruppen-user-grants*
*Completed: 2026-08-04*
## Self-Check: PASSED
Alle 13 in dieser SUMMARY genannten Dateien existieren auf der Platte, beide Task-Commit-Hashes (`43c7fa2`, `2e7daa4`) sind im Git-Log auffindbar.