docs(15-08): complete Serverseitige Modulsperre und Marketplace-Sperr-Badge plan
This commit is contained in:
@@ -511,7 +511,7 @@ Plans:
|
|||||||
|
|
||||||
**Neue Modelle**: `Group` (tenantId, name, ldapDn?), `GroupMembership` (userId, groupId, source MANUAL|LDAP), `ModuleGrant` (tenantId, moduleId, groupId? | userId?)
|
**Neue Modelle**: `Group` (tenantId, name, ldapDn?), `GroupMembership` (userId, groupId, source MANUAL|LDAP), `ModuleGrant` (tenantId, moduleId, groupId? | userId?)
|
||||||
|
|
||||||
**Plans**: 6/8 plans executed
|
**Plans**: 7/8 plans executed
|
||||||
|
|
||||||
Plans:
|
Plans:
|
||||||
**Wave 1**
|
**Wave 1**
|
||||||
@@ -532,7 +532,7 @@ Plans:
|
|||||||
**Wave 4** *(blocked on Wave 3 completion)*
|
**Wave 4** *(blocked on Wave 3 completion)*
|
||||||
|
|
||||||
- [x] 15-07-PLAN.md — Freigabe-Matrix, Benutzer-Detail und Aktivierungsdialog
|
- [x] 15-07-PLAN.md — Freigabe-Matrix, Benutzer-Detail und Aktivierungsdialog
|
||||||
- [ ] 15-08-PLAN.md — Serverseitige 403-Modulsperre und Marketplace-Kennzeichnung
|
- [x] 15-08-PLAN.md — Serverseitige 403-Modulsperre und Marketplace-Kennzeichnung
|
||||||
|
|
||||||
**Wellen**: 1 → 15-01 · 2 → 15-02, 15-04, 15-05 · 3 → 15-03, 15-06 · 4 → 15-07, 15-08
|
**Wellen**: 1 → 15-01 · 2 → 15-02, 15-04, 15-05 · 3 → 15-03, 15-06 · 4 → 15-07, 15-08
|
||||||
|
|
||||||
@@ -559,4 +559,4 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10
|
|||||||
| 12. Tender Notifications | 4/4 | In Progress| |
|
| 12. Tender Notifications | 4/4 | In Progress| |
|
||||||
| 13. Scraping Adapters & Cross-Source Deduplication | 6/6 | In Progress| |
|
| 13. Scraping Adapters & Cross-Source Deduplication | 6/6 | In Progress| |
|
||||||
| 14. RSS, Email-Alert Ingestion & Module Rollout | 5/5 | In Progress| |
|
| 14. RSS, Email-Alert Ingestion & Module Rollout | 5/5 | In Progress| |
|
||||||
| 15. Modul-Berechtigungen: Gruppen & User-Grants | 6/8 | In Progress| |
|
| 15. Modul-Berechtigungen: Gruppen & User-Grants | 7/8 | In Progress| |
|
||||||
|
|||||||
+10
-7
@@ -5,15 +5,15 @@ milestone_name: Ausschreibungs-Radar
|
|||||||
current_phase: 15
|
current_phase: 15
|
||||||
current_phase_name: modul-berechtigungen-gruppen-user-grants
|
current_phase_name: modul-berechtigungen-gruppen-user-grants
|
||||||
status: executing
|
status: executing
|
||||||
stopped_at: Completed 15-07-PLAN.md
|
stopped_at: Completed 15-08-PLAN.md
|
||||||
last_updated: "2026-08-04T17:28:00.601Z"
|
last_updated: "2026-08-04T17:52:39.281Z"
|
||||||
last_activity: 2026-08-04
|
last_activity: 2026-08-04
|
||||||
last_activity_desc: Phase 15 execution started
|
last_activity_desc: Phase 15 execution started
|
||||||
progress:
|
progress:
|
||||||
total_phases: 15
|
total_phases: 15
|
||||||
completed_phases: 13
|
completed_phases: 13
|
||||||
total_plans: 75
|
total_plans: 75
|
||||||
completed_plans: 72
|
completed_plans: 73
|
||||||
---
|
---
|
||||||
|
|
||||||
# Project State
|
# Project State
|
||||||
@@ -28,11 +28,11 @@ See: .planning/PROJECT.md (updated 2026-07-17)
|
|||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Phase: 15 (modul-berechtigungen-gruppen-user-grants) — EXECUTING
|
Phase: 15 (modul-berechtigungen-gruppen-user-grants) — EXECUTING
|
||||||
Plan: 7 of 8
|
Plan: 8 of 8
|
||||||
Status: Ready to execute
|
Status: Ready to execute
|
||||||
Last activity: 2026-08-04 — Phase 15 execution started
|
Last activity: 2026-08-04 — Phase 15 execution started
|
||||||
|
|
||||||
Progress: [██████████] 96%
|
Progress: [██████████] 97%
|
||||||
|
|
||||||
## Performance Metrics
|
## Performance Metrics
|
||||||
|
|
||||||
@@ -106,6 +106,7 @@ Progress: [██████████] 96%
|
|||||||
| Phase 15 P03 | 32min | 3 tasks | 8 files |
|
| Phase 15 P03 | 32min | 3 tasks | 8 files |
|
||||||
| Phase 15 P06 | 35min | 3 tasks | 8 files |
|
| Phase 15 P06 | 35min | 3 tasks | 8 files |
|
||||||
| Phase 15 P07 | 30min | 3 tasks | 7 files |
|
| Phase 15 P07 | 30min | 3 tasks | 7 files |
|
||||||
|
| Phase 15 P08 | 30min | 2 tasks | 12 files |
|
||||||
|
|
||||||
## Accumulated Context
|
## Accumulated Context
|
||||||
|
|
||||||
@@ -251,6 +252,8 @@ Recent decisions affecting current work:
|
|||||||
- [Phase ?]: [15-07]: aria-label des Grant-Checkboxes beschreibt die vom Klick ausgeloeste Aktion (granted: String(!isGranted)), nicht den aktuellen Haekchen-Zustand
|
- [Phase ?]: [15-07]: aria-label des Grant-Checkboxes beschreibt die vom Klick ausgeloeste Aktion (granted: String(!isGranted)), nicht den aktuellen Haekchen-Zustand
|
||||||
- [Phase ?]: [15-07]: UserAccessModal leitet Gruppenmitgliedschafts-Chips ausschliesslich aus der Vereinigung aller viaGroups-Namen von GET /module-grants/users/:userId ab, kein zweiter Endpoint
|
- [Phase ?]: [15-07]: UserAccessModal leitet Gruppenmitgliedschafts-Chips ausschliesslich aus der Vereinigung aller viaGroups-Namen von GET /module-grants/users/:userId ab, kein zweiter Endpoint
|
||||||
- [Phase ?]: [15-07]: ActivateModuleDialog ruft onSuccess bereits nach dem erfolgreichen activate-Call auf, unabhaengig vom Ausgang des nachfolgenden module-grants-Calls
|
- [Phase ?]: [15-07]: ActivateModuleDialog ruft onSuccess bereits nach dem erfolgreichen activate-Call auf, unabhaengig vom Ausgang des nachfolgenden module-grants-Calls
|
||||||
|
- [Phase ?]: [15-08]: isAdmin-Gate auf /marketplace und /marketplace/[slug] entfernt (D-08: Katalog bleibt Schaufenster fuer jeden authentifizierten Benutzer) — isAdmin gated jetzt nur noch die Aktivieren/Deaktivieren-Aktion (canManage-Prop)
|
||||||
|
- [Phase ?]: [15-08]: MarketplaceCard-Klick delegiert an getrennte onOpenDetail/onLockedClick-Callback-Props statt eigener Router-Logik in der Karte — bleibt praesentational und ueber vi.fn() testbar
|
||||||
|
|
||||||
### Pending Todos
|
### Pending Todos
|
||||||
|
|
||||||
@@ -293,7 +296,7 @@ Items acknowledged and carried forward from previous milestone close:
|
|||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-08-04T17:28:00.568Z
|
Last session: 2026-08-04T17:52:39.252Z
|
||||||
Stopped at: Completed 15-07-PLAN.md
|
Stopped at: Completed 15-08-PLAN.md
|
||||||
Resume file: None
|
Resume file: None
|
||||||
Last activity: 2026-07-14 - Built LDAP per-user exclude/denylist filter (9d1323f), migration applied on live DB, verified via Playwright: sync deactivated 4 excluded service accounts (administrator/krbtgt/guest/dns-ldap), 2 real LDAP users stay active, 0 wrongly created
|
Last activity: 2026-07-14 - Built LDAP per-user exclude/denylist filter (9d1323f), migration applied on live DB, verified via Playwright: sync deactivated 4 excluded service accounts (administrator/krbtgt/guest/dns-ldap), 2 real LDAP users stay active, 0 wrongly created
|
||||||
|
|||||||
+16
-3
@@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
schema_version: 1
|
schema_version: 1
|
||||||
open_count: 2
|
open_count: 3
|
||||||
waived_count: 0
|
waived_count: 0
|
||||||
fixed_count: 0
|
fixed_count: 0
|
||||||
total_count: 2
|
total_count: 3
|
||||||
last_updated: 2026-08-04T17:27:42.267Z
|
last_updated: 2026-08-04T17:50:13.320Z
|
||||||
---
|
---
|
||||||
|
|
||||||
# Broken Windows Ledger
|
# Broken Windows Ledger
|
||||||
@@ -17,6 +17,7 @@ last_updated: 2026-08-04T17:27:42.267Z
|
|||||||
|----|-------|------|------|------|-------------|--------|--------|-------------|-------------|
|
|----|-------|------|------|------|-------------|--------|--------|-------------|-------------|
|
||||||
| 1 | 15 | unrun-verify | .planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-06-PLAN.md | | 15-06 <verification>: manueller Browser-Durchklick (Anlegen/Umbenennen/Standardmarkierung/AD-Bindung/Mitglieder/Loeschdialog + Fehlerpfade bei abgeschalteter API) nicht ausgefuehrt — kein Browser-Tool in dieser Session verfuegbar | open | | 2026-08-04T17:05:42.026Z | |
|
| 1 | 15 | unrun-verify | .planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-06-PLAN.md | | 15-06 <verification>: manueller Browser-Durchklick (Anlegen/Umbenennen/Standardmarkierung/AD-Bindung/Mitglieder/Loeschdialog + Fehlerpfade bei abgeschalteter API) nicht ausgefuehrt — kein Browser-Tool in dieser Session verfuegbar | open | | 2026-08-04T17:05:42.026Z | |
|
||||||
| 2 | 15 | unrun-verify | .planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-07-PLAN.md | | Manueller Browser-Durchklick aus dem Plan-Verification-Block (Matrix-Freigabe setzen/entziehen, Aktivierungsdialog beide Wege, Direkt-Grant neben Gruppen-Grant, Fehlerfall bei gestoppter API, lange Namen) nicht ausgefuehrt -- kein Browser-Tool in dieser Session (15-07-SUMMARY.md D4) | open | | 2026-08-04T17:27:42.267Z | |
|
| 2 | 15 | unrun-verify | .planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-07-PLAN.md | | Manueller Browser-Durchklick aus dem Plan-Verification-Block (Matrix-Freigabe setzen/entziehen, Aktivierungsdialog beide Wege, Direkt-Grant neben Gruppen-Grant, Fehlerfall bei gestoppter API, lange Namen) nicht ausgefuehrt -- kein Browser-Tool in dieser Session (15-07-SUMMARY.md D4) | open | | 2026-08-04T17:27:42.267Z | |
|
||||||
|
| 3 | 15 | unrun-verify | .planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-08-PLAN.md | | Manueller Browser-Durchklick aus dem Plan-Verification-Block (USER ohne Freigabe: Sidebar/403-Seite/Marketplace-Badge+Toast/API-403 identisch; ADMIN: alle vier Ebenen zugaenglich) nicht ausgefuehrt - kein Browser-Tool in dieser Session verfuegbar. | open | | 2026-08-04T17:50:13.320Z | |
|
||||||
|
|
||||||
````json
|
````json
|
||||||
[
|
[
|
||||||
@@ -43,6 +44,18 @@ last_updated: 2026-08-04T17:27:42.267Z
|
|||||||
"reason": "",
|
"reason": "",
|
||||||
"recorded_at": "2026-08-04T17:27:42.267Z",
|
"recorded_at": "2026-08-04T17:27:42.267Z",
|
||||||
"resolved_at": null
|
"resolved_at": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"kind": "unrun-verify",
|
||||||
|
"phase": "15",
|
||||||
|
"file": ".planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-08-PLAN.md",
|
||||||
|
"line": null,
|
||||||
|
"description": "Manueller Browser-Durchklick aus dem Plan-Verification-Block (USER ohne Freigabe: Sidebar/403-Seite/Marketplace-Badge+Toast/API-403 identisch; ADMIN: alle vier Ebenen zugaenglich) nicht ausgefuehrt - kein Browser-Tool in dieser Session verfuegbar.",
|
||||||
|
"status": "open",
|
||||||
|
"reason": "",
|
||||||
|
"recorded_at": "2026-08-04T17:50:13.320Z",
|
||||||
|
"resolved_at": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
````
|
````
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
Reference in New Issue
Block a user