Files

18 KiB
Raw Permalink Blame History

phase, plan, subsystem, tags, requires, provides, affects, actuals, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, coverage, duration, completed, status
phase plan subsystem tags requires provides affects actuals tech-stack key-files key-decisions patterns-established requirements-completed coverage duration completed status
15-modul-berechtigungen-gruppen-user-grants 08 ui
nextjs
next-intl
react
server-components
marketplace
module-access
phase plan provides
15-modul-berechtigungen-gruppen-user-grants 01 ModuleAccessService.getAccessibleModuleIds als Single Source of Truth (D-01), GET /modules/active auf Benutzer-Ebene
phase plan provides
15-modul-berechtigungen-gruppen-user-grants 03 GET /modules/catalog mit isActiveForTenant/hasAccess in einer Antwort (D-08)
phase plan provides
15-modul-berechtigungen-gruppen-user-grants 06 i18n-Schluessel modules.accessDenied.*, marketplace.statusNoAccess/toastNoAccess
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
tokens tasks commits
13400 2 2
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
created modified
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
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
[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
checkModuleAccess als zweite Server-Action neben fetchCurrentUser, identisches Cookie-Weiterleitungs- und Fail-closed-Muster
PERM-04
id description requirement verification human_judgment
D1 checkModuleAccess prueft serverseitig gegen GET /modules/active, schliesst bei fehlendem Cookie/nicht-ok-Antwort/Fehler im Zweifel (T-15-29) PERM-04
kind ref status
unit apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/module-access.test.tsx — 'checkModuleAccess — fails closed (T-15-29)' (2 Tests) pass
false
id description requirement verification human_judgment
D2 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 PERM-04
kind ref status
unit module-access.test.tsx — 'ExpandedModulePage — server access gate' (2 Tests) + 'ModuleShell — whitelist stays independent' (1 Test) pass
kind ref status
other 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 pass
false
id description requirement verification human_judgment
D3 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) PERM-04
kind ref status
unit 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) pass
kind ref status
other grep-Nachweise aus Task 2 acceptance_criteria: hasAccess>=3, statusNoAccess>=1, amber>=2 (Hell-/Dunkelvariante), flex-wrap>=1 pass
false
id description requirement verification human_judgment
D4 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 PERM-04
kind ref status
unit marketplace.test.tsx, marketplace-filters.test.tsx, tenant-selector.test.tsx, detail.test.tsx — alle auf GET /modules/catalog-Mock umgestellt, 26 Tests gruen pass
kind ref status
other grep-Nachweise: modules/catalog in beiden Dateien, fetch(-Vorkommen in marketplace/page.tsx um 1 gesunken (3 -> 2) pass
false
id description verification human_judgment rationale
D5 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
true 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).
30min 2026-08-04 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.