From 2a79d4ca1f83f39c34d434ff1d300049ba75e828 Mon Sep 17 00:00:00 2001 From: Schalli Date: Tue, 4 Aug 2026 15:39:48 +0200 Subject: [PATCH] docs(15-05): complete dashboard-widget-modulfilterung plan --- .planning/REQUIREMENTS.md | 4 +- .planning/ROADMAP.md | 6 +- .planning/STATE.md | 17 ++- .../15-05-SUMMARY.md | 143 ++++++++++++++++++ 4 files changed, 158 insertions(+), 12 deletions(-) create mode 100644 .planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-05-SUMMARY.md diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 22a4879..97319a7 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -69,7 +69,7 @@ - [x] **PERM-04**: Ohne Freigabe hat ein USER keinen Zugriff: das Modul fehlt in der Sidebar, die Modulseite antwortet mit einer 403-Seite samt Hinweis, und die Modul-API antwortet mit 403. Sidebar, Modulseite und API nutzen dieselbe Zugriffsauflösung. - [x] **PERM-05**: ADMIN und SUPER_ADMIN sehen und nutzen innerhalb ihres Mandanten alle aktiven Module ohne Freigabe. - [x] **PERM-06**: Die Migration überführt den Bestand ohne Zugriffsverlust: pro Mandant entsteht eine als Standardgruppe markierte Gruppe mit allen bestehenden Benutzern und Freigaben für alle zum Migrationszeitpunkt aktiven Module. Neue Benutzer — manuell angelegt wie per LDAP importiert — treten der markierten Standardgruppe automatisch bei. -- [ ] **PERM-07**: Ein Dashboard-Widget, dessen Modul dem Benutzer nicht freigegeben ist, erscheint nicht auf seinem Dashboard. +- [x] **PERM-07**: Ein Dashboard-Widget, dessen Modul dem Benutzer nicht freigegeben ist, erscheint nicht auf seinem Dashboard. ## Future Requirements (deferred) @@ -128,6 +128,6 @@ | PERM-04 | Phase 15 | Pending | | PERM-05 | Phase 15 | Pending | | PERM-06 | Phase 15 | Pending | -| PERM-07 | Phase 15 | Pending | +| PERM-07 | Phase 15 | Complete | **Coverage:** 29/29 v1.1 requirements mapped — no orphans. 7/7 v1.2 requirements mapped auf Phase 15. diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 16af5ff..bcb1113 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -511,7 +511,7 @@ Plans: **Neue Modelle**: `Group` (tenantId, name, ldapDn?), `GroupMembership` (userId, groupId, source MANUAL|LDAP), `ModuleGrant` (tenantId, moduleId, groupId? | userId?) -**Plans**: 2/8 plans executed +**Plans**: 3/8 plans executed Plans: **Wave 1** @@ -522,7 +522,7 @@ Plans: - [x] 15-02-PLAN.md — Gruppen-API und automatische Standardgruppen-Mitgliedschaft - [ ] 15-04-PLAN.md — AD-Gruppenbindung im bestehenden LDAP-Sync -- [ ] 15-05-PLAN.md — Modulfilter für Dashboard-Widgets +- [x] 15-05-PLAN.md — Modulfilter für Dashboard-Widgets **Wave 3** *(blocked on Wave 2 completion)* @@ -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| | | 13. Scraping Adapters & Cross-Source Deduplication | 6/6 | In Progress| | | 14. RSS, Email-Alert Ingestion & Module Rollout | 5/5 | In Progress| | -| 15. Modul-Berechtigungen: Gruppen & User-Grants | 2/8 | In Progress| | +| 15. Modul-Berechtigungen: Gruppen & User-Grants | 3/8 | In Progress| | diff --git a/.planning/STATE.md b/.planning/STATE.md index 79d2e67..3421e8c 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,15 +5,15 @@ milestone_name: Ausschreibungs-Radar current_phase: 15 current_phase_name: modul-berechtigungen-gruppen-user-grants status: executing -stopped_at: Completed 15-02-PLAN.md -last_updated: "2026-08-04T13:27:34.942Z" +stopped_at: Completed 15-05-PLAN.md +last_updated: "2026-08-04T13:39:18.710Z" last_activity: 2026-08-04 last_activity_desc: Phase 15 execution started progress: total_phases: 15 completed_phases: 13 total_plans: 75 - completed_plans: 68 + completed_plans: 69 --- # Project State @@ -28,11 +28,11 @@ See: .planning/PROJECT.md (updated 2026-07-17) ## Current Position Phase: 15 (modul-berechtigungen-gruppen-user-grants) — EXECUTING -Plan: 3 of 8 +Plan: 4 of 8 Status: Ready to execute Last activity: 2026-08-04 — Phase 15 execution started -Progress: [█████████░] 91% +Progress: [█████████░] 92% ## Performance Metrics @@ -102,6 +102,7 @@ Progress: [█████████░] 91% | Phase 14 P05 | 50min | 3 tasks | 19 files | | Phase 15 P01 | 24min | 3 tasks | 10 files | | Phase 15-modul-berechtigungen-gruppen-user-grants P02 | 11min | 2 tasks | 11 files | +| Phase 15 P05 | 9min | 2 tasks | 5 files | ## Accumulated Context @@ -236,6 +237,8 @@ Recent decisions affecting current work: - [Phase ?]: [15-02]: isDefault:true läuft in this.prisma.$transaction([updateMany, update]); partieller Unique-Index aus 15-01 bleibt Sicherheitsnetz - [Phase ?]: [15-02]: remove() fängt zusätzlich P2025 ab (NotFoundException statt unbehandeltem 500) — Rule 2, für Concurrency-Anforderung aus must_haves - [Phase ?]: [15-02]: UserService.create ist die einzige Codestelle für D-11/D-12 — LdapService erbt die Regel ohne eigene Kopie (ldap.service.ts unverändert) +- [Phase ?]: [15-05]: WIDGET_MODULE_MAP bleibt am Ende dieser Phase bewusst leer (D-22) — kein neuer Widget-Typ, keine Schemaänderung, nur die Filtermechanik +- [Phase ?]: [15-05]: vi.hoisted() für die je-Testfall mutierbare WIDGET_MODULE_MAP-Mock-Referenz — vi.mock wird an den Dateianfang gehoben, ein normaler top-level const wäre zur Factory-Ausführungszeit noch nicht initialisiert ### Pending Todos @@ -277,7 +280,7 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-08-04T13:27:34.911Z -Stopped at: Completed 15-02-PLAN.md +Last session: 2026-08-04T13:39:18.682Z +Stopped at: Completed 15-05-PLAN.md 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 diff --git a/.planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-05-SUMMARY.md b/.planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-05-SUMMARY.md new file mode 100644 index 0000000..7e6ce32 --- /dev/null +++ b/.planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-05-SUMMARY.md @@ -0,0 +1,143 @@ +--- +phase: 15-modul-berechtigungen-gruppen-user-grants +plan: 05 +subsystem: dashboard +tags: [nestjs, prisma, dashboard, module-access, vitest] + +# Dependency graph +requires: + - phase: 15-modul-berechtigungen-gruppen-user-grants + plan: 01 + provides: ModuleAccessService.getAccessibleModuleIds als Single Source of Truth für Modulzugriff (D-01) +provides: + - WIDGET_MODULE_MAP/getModuleSlugForWidgetType — statische Widget-Typ-zu-Modul-Zuordnung (D-22), am Ende dieser Phase bewusst leer + - DashboardService.getWidgets(userId, tenantId, role) mit Modulfilter über ModuleAccessService + - Erste Testsuite für DashboardService überhaupt (dashboard.service.spec.ts) +affects: [] + +actuals: + tokens: 4000 + tasks: 2 + commits: 3 + +tech-stack: + added: [] + patterns: + - "vi.hoisted() für eine mutierbare Mock-Referenz, wenn ein per-Testfall veränderbarer Modulinhalt über vi.mock benötigt wird — vi.mock selbst wird von Vitest an den Dateianfang gehoben, ein normaler top-level const wäre zur Factory-Ausführungszeit noch nicht initialisiert" + - "Bedingter Zugriffs-Lookup: erst die bestehende Query unverändert ausführen, dann nur bei mindestens einem Treffer in der Zuordnungstabelle die teurere Zugriffsauflösung aufrufen — hält den Leerlauf-Fall (heute: immer) auf Kosten einer einzigen bestehenden Query" + +key-files: + created: + - apps/api/src/dashboard/widget-module-map.ts + - apps/api/src/dashboard/dashboard.service.spec.ts + modified: + - apps/api/src/dashboard/dashboard.service.ts + - apps/api/src/dashboard/dashboard.controller.ts + - apps/api/src/dashboard/dashboard.module.ts + +key-decisions: + - "WIDGET_MODULE_MAP bleibt am Ende dieser Phase bewusst leer (D-22, 15-RESEARCH.md Pitfall 5) — alle acht bestehenden Widget-Typen sind Plattform-Widgets ohne Modulbezug, kein neuer Widget-Typ entsteht in diesem Plan" + - "Code-Konstante statt Datenbankspalte auf WidgetInstance — eine Migration auf einer bereits befüllten Tabelle für ein Feld, das derzeit für jede Zeile leer wäre, wiegt schwerer als eine TypeScript-Konstante mit identischer Aussagekraft" + +requirements-completed: [PERM-07] + +coverage: + - id: D1 + description: "WIDGET_MODULE_MAP + getModuleSlugForWidgetType als einziger Lesezugriff auf die Zuordnungstabelle, ohne Schemaänderung und ohne neuen Widget-Typ" + requirement: "PERM-07" + verification: + - kind: unit + ref: "pnpm --filter @tessera/api run type-check — fehlerfrei" + status: pass + - kind: manual_procedural + ref: "grep -c widgetType apps/api/prisma/schema.prisma und ls apps/api/prisma/migrations/ | wc -l vor/nach diesem Task unverändert (1 bzw. 25)" + status: pass + human_judgment: false + - id: D2 + description: "DashboardService.getWidgets(userId, tenantId, role) filtert Widgets eines gesperrten Moduls serverseitig heraus, mit genau einem Aufruf von ModuleAccessService.getAccessibleModuleIds und Fail-Closed bei unauflösbarem Modul-Slug" + requirement: "PERM-07" + verification: + - kind: unit + ref: "apps/api/src/dashboard/dashboard.service.spec.ts (8 Tests, deckt jeden -Fall inkl. adjacency/empty/ordering/idempotency und D-03-ADMIN-Kurzschluss ab)" + status: pass + - kind: e2e + ref: "curl gegen laufende lokale API + DB-Container: mit leerer WIDGET_MODULE_MAP liefert GET /dashboard/widgets für einen eigens angelegten Testbenutzer dieselben 2 Widgets (clock, search), die vor dem Aufruf angelegt wurden — kein Bestandswidget verschwindet" + status: pass + human_judgment: false + +duration: 9min +completed: 2026-08-04 +status: complete +--- + +# Phase 15 Plan 05: Dashboard-Widget-Modulfilterung Summary + +**Statische `WIDGET_MODULE_MAP` (bewusst leer) plus `DashboardService.getWidgets(userId, tenantId, role)`, das Widgets eines für den Benutzer gesperrten Moduls über `ModuleAccessService.getAccessibleModuleIds` (D-01, aus Plan 15-01) herausfiltert, mit Fail-Closed bei unauflösbarem Modul-Slug — die erste Testsuite für `DashboardService` überhaupt, mit End-to-End-Nachweis gegen die laufende lokale API.** + +## Performance + +- **Duration:** 9 min +- **Started:** 2026-08-04T13:28:00Z +- **Completed:** 2026-08-04T13:37:28Z +- **Tasks:** 2 +- **Files modified:** 5 + +## Accomplishments + +- `apps/api/src/dashboard/widget-module-map.ts` (neu): exportiert `WIDGET_MODULE_MAP` (`Readonly>`) und `getModuleSlugForWidgetType`, mit einem Blockkommentar, der Zweck, Schlüssel/Werte-Bedeutung und die bewusste Entscheidung gegen eine Schemaspalte festhält. Die Tabelle ist zum Abschluss dieser Phase leer — alle acht heute registrierten Widget-Typen (clock/search/calendar/note/calculator/favorites/link/stopwatch) sind Plattform-Widgets ohne Modulbezug +- `DashboardService.getWidgets` erweitert von `(userId)` auf `(userId, tenantId, role: Role)`: die bestehende `findMany`-Query mit `orderBy: { createdAt: 'asc' }` läuft unverändert zuerst; nur wenn mindestens ein geladenes Widget in `WIDGET_MODULE_MAP` steht, wird `ModuleAccessService.getAccessibleModuleIds` genau einmal aufgerufen (kein Lookup je Widget) und die betroffenen Modul-Slugs über eine einzelne `module.findMany`-Query auf IDs abgebildet; Widgets ohne Tabelleneintrag bleiben immer erhalten, Widgets mit unauflösbarem Modul-Slug werden entfernt (Fail-Closed) +- `DashboardController.getWidgets` reicht `tenantId` und `role` (aus `req.user`, JWT-Herkunft) zusätzlich zu `userId` durch +- `DashboardModule` importiert `ModuleRegistryModule`, damit `ModuleAccessService` injizierbar wird +- `dashboard.service.spec.ts` (neu, 8 Tests): deckt jeden ``-Fall aus dem Plan ab — leere Tabelle liefert exakt die Prisma-Menge ohne Zugriffs-Lookup, sichtbar bei Zugriff, entfernt ohne Zugriff, D-03-ADMIN-Kurzschluss, Nicht-Tabellen-Typen bleiben immer erhalten (adjacency), Sortierung bleibt nach dem Filtern erhalten (ordering), zwei Aufrufe löschen keine Zeile (idempotency), Fail-Closed bei fehlendem `Module`-Datensatz zu einem eingetragenen Slug. `WIDGET_MODULE_MAP` wird je Testfall über eine mit `vi.hoisted()` deklarierte, gemockte Objektreferenz gesteuert (plain `const` wäre zur Ausführungszeit der gehobenen `vi.mock`-Factory noch nicht initialisiert) +- End-to-End-Nachweis gegen die laufende lokale API (`pnpm start:dev` gegen den DB-Container, wie in 15-01/15-02): ein eigens per `psql` angelegter Testbenutzer mit zwei Plattform-Widgets (`clock`, `search`) erhält bei leerer `WIDGET_MODULE_MAP` über `GET /dashboard/widgets` exakt dieselben 2 Widgets zurück — Testbenutzer und -Widgets nach dem Nachweis wieder aus der DB entfernt + +## Task Commits + +Jeder Task wurde atomar committet: + +1. **Task 1: Statische Widget-Modul-Zuordnung anlegen** - `954cd6e` (feat) +2. **Task 2: getWidgets filtert über dieselbe Zugriffsauflösung wie Guard und Sidebar** - `d0ff6f0` (test, RED) + `0ff46ac` (feat, GREEN) + +**Plan metadata:** siehe Commit dieser SUMMARY.md (docs: complete plan) + +## Files Created/Modified + +- `apps/api/src/dashboard/widget-module-map.ts` - `WIDGET_MODULE_MAP` (leer) + `getModuleSlugForWidgetType` (neu) +- `apps/api/src/dashboard/dashboard.service.ts` - `getWidgets` um Modulfilter erweitert, `ModuleAccessService` injiziert +- `apps/api/src/dashboard/dashboard.controller.ts` - `getWidgets`-Handler reicht `tenantId`/`role` durch +- `apps/api/src/dashboard/dashboard.module.ts` - Import von `ModuleRegistryModule` +- `apps/api/src/dashboard/dashboard.service.spec.ts` - 8 Tests (neu — erste Testsuite für `DashboardService`) + +## Decisions Made + +- `WIDGET_MODULE_MAP` bleibt bewusst leer (kein neuer Widget-Typ, keine Schemaänderung) — wie im Plan-Objective vorgegeben, kein Abweichen +- `vi.hoisted()` für die je-Testfall mutierbare `WIDGET_MODULE_MAP`-Mock-Referenz — nicht explizit im Plan-Action-Text benannt, aber notwendig, weil Vitest `vi.mock`-Aufrufe an den Dateianfang hebt und ein normaler `const mockMap = {}` zur Ausführungszeit der Factory noch nicht initialisiert wäre (verifiziert: erster Testlauf ohne `vi.hoisted()` schlug mit `ReferenceError: Cannot access 'mockMap' before initialization` fehl) + +## Deviations from Plan + +None - plan wie geschrieben ausgeführt. + +## Issues Encountered + +- Beim End-to-End-Nachweis lief zunächst ein API-Prozess aus einer vorherigen Verifikationsrunde ohne `JWT_SECRET` weiter im Hintergrund (Port 3001 belegt, `EADDRINUSE`); nach `pkill -f "nest start --watch"` und Prüfung, dass Port 3001 wieder frei ist, startete der Nachweis sauber durch — kein Einfluss auf den eigentlichen Code oder die Testsuite +- Es existierte kein Bestandsbenutzer mit bekanntem Passwort in der lokalen DB (nur der SUPER_ADMIN aus einer vorherigen Sitzung, `AdminSeedService` übersprang das Seeding mangels `TESSERA_ADMIN_*`-Env-Vars). Für den E2E-Nachweis wurde per `psql` ein isolierter Testbenutzer (`argon2`-Hash über `node -e`) samt zwei Widgets angelegt, gegen die laufende lokale API eingeloggt und nach dem Nachweis vollständig wieder gelöscht — analog zum in 15-02 etablierten Verfahren für den Fremdmandanten-Nachweis + +## User Setup Required + +None - keine externe Service-Konfiguration nötig. + +## Next Phase Readiness + +- Die Mechanik aus D-22 ist vollständig, getestet und end-to-end bewiesen (PERM-07) +- Bei leerer Zuordnungstabelle verändert sich für keinen Bestandsbenutzer etwas — durch Unit-Test (`empty`-Fall) und E2E-Nachweis doppelt belegt +- Sobald ein künftiges modulgebundenes Widget entsteht, genügt ein Eintrag in `WIDGET_MODULE_MAP` — die Filterlogik selbst braucht keine Änderung +- Kein offener Blocker aus diesem Plan +- Voller API-Testsuite: 457/457 grün (449 aus 15-02 + 8 neue) + +--- +*Phase: 15-modul-berechtigungen-gruppen-user-grants* +*Completed: 2026-08-04* + +## Self-Check: PASSED + +Alle in dieser SUMMARY genannten Dateien existieren auf der Platte, alle genannten Commit-Hashes (`954cd6e`, `d0ff6f0`, `0ff46ac`) sind im Git-Log auffindbar.