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

9.7 KiB

phase, plan, subsystem, tags, requires, provides, affects, actuals, tech-stack, key-files, key-decisions, requirements-completed, coverage, duration, completed, status
phase plan subsystem tags requires provides affects actuals tech-stack key-files key-decisions requirements-completed coverage duration completed status
15-modul-berechtigungen-gruppen-user-grants 05 dashboard
nestjs
prisma
dashboard
module-access
vitest
phase plan provides
15-modul-berechtigungen-gruppen-user-grants 01 ModuleAccessService.getAccessibleModuleIds als Single Source of Truth für Modulzugriff (D-01)
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)
tokens tasks commits
4000 2 3
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
created modified
apps/api/src/dashboard/widget-module-map.ts
apps/api/src/dashboard/dashboard.service.spec.ts
apps/api/src/dashboard/dashboard.service.ts
apps/api/src/dashboard/dashboard.controller.ts
apps/api/src/dashboard/dashboard.module.ts
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
PERM-07
id description requirement verification human_judgment
D1 WIDGET_MODULE_MAP + getModuleSlugForWidgetType als einziger Lesezugriff auf die Zuordnungstabelle, ohne Schemaänderung und ohne neuen Widget-Typ PERM-07
kind ref status
unit pnpm --filter @tessera/api run type-check — fehlerfrei pass
kind ref status
manual_procedural 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) pass
false
id description requirement verification human_judgment
D2 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 PERM-07
kind ref status
unit apps/api/src/dashboard/dashboard.service.spec.ts (8 Tests, deckt jeden <behavior>-Fall inkl. adjacency/empty/ordering/idempotency und D-03-ADMIN-Kurzschluss ab) pass
kind ref status
e2e 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 pass
false
9min 2026-08-04 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<Record<string, string>>) 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 <behavior>-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.