Files
schalli c2e9f67a7d
Tessera CI/CD / Lint & Type Check (push) Successful in 50s
Tessera CI/CD / Tests (push) Successful in 52s
Tessera CI/CD / Build & Publish Images (push) Successful in 4m5s
docs(quick-260805-d0r): Gruppenmitgliedschaften im Benutzer-Detail
2026-08-05 09:44:11 +02:00

125 lines
8.2 KiB
Markdown

---
phase: quick-260805-d0r
plan: 01
subsystem: admin-users
tags: [module-grants, groups, group-membership, i18n-reuse]
status: complete
dependency-graph:
requires:
- GroupMembership (Prisma-Modell, Phase 15-01)
- ModuleGrantsService.getUserAccess (Phase 15-07)
provides:
- "GET /module-grants/users/:userId liefert { groups, modules } statt eines Arrays"
- "UserAccessModal-Gruppenmitgliedschafts-Chips aus der tatsaechlichen Mitgliedschaft"
affects:
- apps/api/src/groups/module-grants.service.ts
- apps/api/src/groups/module-grants.controller.ts
- "apps/web/src/app/(portal)/admin/users/components/UserAccessModal.tsx"
tech-stack:
added: []
patterns:
- "Herkunfts-Badge-Markup 1:1 aus GroupMembersModal.tsx uebernommen statt einer zweiten Variante"
key-files:
created: []
modified:
- apps/api/src/groups/module-grants.service.ts
- apps/api/src/groups/module-grants.service.spec.ts
- apps/api/src/groups/module-grants.controller.ts
- "apps/web/src/app/(portal)/admin/users/components/UserAccessModal.tsx"
- "apps/web/src/app/(portal)/admin/users/user-access-modal.test.tsx"
decisions:
- "Kein forTenant() fuer die neue groupMembership.findMany-Abfrage — bewusste Abweichung vom urspruenglichen Auftrag, ausfuehrlich im PLAN.md-Objective begruendet (RLS wird von der DB-Rolle ohnehin umgangen, der `where`-Filter ist wie bei den drei bestehenden Abfragen derselben Methode der primaere Schutz). Der Browser-Gegenprobe-Schritt (Task 3) ist das vereinbarte Gegenzeichen, falls Chips trotz vorhandener GroupMembership-Zeilen leer blieben."
- "Kein neuer i18n-Schluessel fuer das Herkunfts-Badge — admin.groups.members.sourceManual/.sourceLdap wird ueber einen zweiten useTranslations-Hook (tMembers) wiederverwendet."
metrics:
duration: "ca. 55 min"
completed: "2026-08-05"
actuals:
tokens: 5944
tasks: 2
commits: 5
---
# Phase quick-260805-d0r Plan 01: Benutzer-Detail zeigt Gruppenmitgliedschaften Summary
Das Benutzer-Detail in /admin/users zeigt Gruppenmitgliedschaften jetzt aus den tatsaechlichen GroupMembership-Zeilen, unabhaengig davon, ob die Gruppe gerade ein Modul freigibt.
## Was gebaut wurde
**Task 1 — Mitgliedschaften end-to-end (D-16):**
`ModuleGrantsService.getUserAccess` liefert jetzt ein Objekt `{ groups, modules }` statt des bisherigen Arrays. `groups` kommt aus einer neuen `this.prisma.groupMembership.findMany`-Abfrage mit `where: { userId, group: { tenantId } }` — dem expliziten Mandantenfilter ueber die Relation, weil `GroupMembership` keine eigene `tenantId`-Spalte traegt. `assertTargetBelongsToTenant` bleibt unveraendert die erste Anweisung der Methode. `modules` ist feldgleich zum bisherigen Rueckgabewert (`{ module, viaGroups, direct }` je aktivem Modul).
Die API-Spec (`module-grants.service.spec.ts`) bekam einen `groupMembership`-Zweig im Hand-Fake-Prisma, einen dritten optionalen `source`-Parameter fuer `__seedMembership` (Default `MANUAL`) und sieben neue/umgestellte Tests: den Regressionsfall ("Mitglied einer Gruppe ohne Modul-Freigabe bleibt sichtbar"), den Cross-Tenant-Fall, den LDAP-Herkunfts-Fall, den Leer-Modul-Fall und den Sortierungs-Fall.
`UserAccessModal.tsx` liest die Gruppen-Chips jetzt direkt aus `data.groups` (React-`key` ist die Gruppen-ID statt des Namens) statt sie aus der Vereinigung aller `row.viaGroups` abzuleiten. Das `useMemo`, das die Namen bisher zusammensetzte, und der ungenutzte `useMemo`-Import wurden entfernt. Der Kopfkommentar der Datei wurde neu formuliert — der alte Satz ("Chips sind die deduplizierte Menge aller viaGroups-Namen, ein zweiter Endpoint existiert bewusst nicht") widersprach sonst dem neuen Code.
Der Controller-JSDoc ueber `GET /module-grants/users/:userId` wurde an die Zwei-Schluessel-Antwort angepasst; der Controller-Code selbst blieb unveraendert (reicht die Service-Antwort weiter).
**Task 2 — Herkunfts-Badge (D-19/D-20):**
Ein zweiter `useTranslations('admin.groups.members')`-Hook (`tMembers`) liest die bereits bestehenden Schluessel `sourceManual`/`sourceLdap`. Jeder Chip zeigt jetzt zusaetzlich zum Gruppennamen ein Badge — Markup und Klassen 1:1 aus `GroupMembersModal.tsx` (Zeilen 176-184) uebernommen: blaue Variante bei `LDAP`, neutral-grau sonst. `de.json`/`en.json` blieben unveraendert — kein neuer Schluessel, deckungsgleiche Pruefung bestand mit 762 Schluesseln auf beiden Seiten.
**Task 3 — Browser-Gegenprobe:** delegiert an den Orchestrator. Der lokale Stack (web:3000, api:3001, db) laeuft; die Browser-Verifikation aus dem Plan (Chip bleibt nach Entzug aller Modul-Freigaben stehen, Leerzustand bei Benutzer ohne Mitgliedschaft, Badges sichtbar) wurde in dieser Ausfuehrung NICHT durchgefuehrt und muss vom Orchestrator nachgeholt werden.
## TDD-Ablauf
Beide Tasks liefen als echte RED/GREEN-Zyklen mit getrennten Commits:
1. `b6d4e4a` test: Service-Spec-Tests ergaenzt, gegen den unveraenderten Service rot (7 von 25 Tests fehlgeschlagen, bestaetigt).
2. `ecadf69` feat: Service + Controller implementiert, 25/25 gruen.
3. `771f680` test: Web-Test-Fixtures auf `{ groups, modules }` umgestellt, gegen das unveraenderte Modal rot (8 von 8 Tests fehlgeschlagen mit `TypeError: (rows ?? []) is not iterable`, bestaetigt).
4. `f8ff74b` feat: Modal implementiert, 7/7 gruen (die achte, das Badge betreffende Testerwartung, wurde vor diesem Commit wieder entfernt — siehe Deviations).
5. `73122b5` test: Badge-Test separat ergaenzt, rot bestaetigt (1 von 8 Tests fehlgeschlagen).
6. `8ce3748` feat: Badge implementiert, 8/8 gruen.
## Deviations from Plan
### Auto-fixed Issues
**1. [Prozess-Korrektur, kein Rule-1/2/3-Fix] Badge-Test versehentlich vorzeitig in Task-1-RED-Commit aufgenommen**
- **Gefunden bei:** Schreiben des Web-Testfiles fuer Task 1.
- **Problem:** Beim ersten Entwurf der neuen `user-access-modal.test.tsx` wurde faelschlich bereits der Badge-Test (Task-2-Umfang) samt `admin.groups.members`-Mock-Namensraum mit reingeschrieben und im RED-Commit `771f680` mitcommittet.
- **Fix:** Vor dem GREEN-Commit von Task 1 wieder entfernt (Namensraum-Eintrag und Testblock), damit Task 1s Commit-Grenze exakt dem Plan entspricht. Der Badge-Test wurde danach in Task 2 als eigener RED/GREEN-Zyklus neu geschrieben.
- **Dateien:** `apps/web/src/app/(portal)/admin/users/user-access-modal.test.tsx`
- **Commits:** Entfernung Teil von `f8ff74b`; Neuanlage in `73122b5`/`8ce3748`.
Keine weiteren Abweichungen — der Plan wurde ansonsten exakt wie beschrieben umgesetzt, inklusive der bewussten Nicht-Verwendung von `forTenant()` gemaess der Begruendung im PLAN.md-Objective.
### Auth-Gates
Keine.
## Known Stubs
Keine.
## Threat Flags
Keine — die im Plan dokumentierten Threats (T-d0r-01/02/03) sind durch die umgesetzte Mandanten-Gegenpruefung und das explizite `group: { tenantId }`-Filter vollstaendig mitigiert bzw. wie geplant akzeptiert; kein zusaetzliches, ungeplantes Angriffsflaechen-Element wurde eingefuehrt.
## Verifikation
- `pnpm --filter @tessera/api test -- module-grants`: 25/25 gruen (inkl. Regression, Cross-Tenant, LDAP-Herkunft).
- `pnpm --filter @tessera/api run type-check`: sauber.
- `pnpm --filter @tessera/web test -- user-access-modal`: 8/8 gruen (inkl. Regression, beider Herkunftsbadges).
- `pnpm --filter @tessera/web run type-check`: sauber.
- `pnpm --filter @tessera/api test`: 498/498 gruen (keine Kollateralschaeden).
- `pnpm --filter @tessera/web test`: 187/187 gruen (keine Kollateralschaeden).
- `de.json`/`en.json`: unveraendert, 762 Schluessel deckungsgleich; `admin.groups.members.sourceManual`/`.sourceLdap` in beiden vorhanden.
- Diff seit Phase-15-Ende (`be1183a..HEAD`) beruehrt exakt die fuenf im Plan gelisteten Dateien, keine Schemaaenderung, keine Migration.
- **Task 3 (Browser-Gegenprobe): OFFEN.** Delegiert an den Orchestrator gemaess Auftrag — bitte vor Abschluss nachholen (siehe orchestrator_decisions im Prompt dieser Ausfuehrung).
## Self-Check: PASSED
- `apps/api/src/groups/module-grants.service.ts`: FOUND
- `apps/api/src/groups/module-grants.service.spec.ts`: FOUND
- `apps/api/src/groups/module-grants.controller.ts`: FOUND
- `apps/web/src/app/(portal)/admin/users/components/UserAccessModal.tsx`: FOUND
- `apps/web/src/app/(portal)/admin/users/user-access-modal.test.tsx`: FOUND
- `b6d4e4a`: FOUND
- `ecadf69`: FOUND
- `771f680`: FOUND
- `f8ff74b`: FOUND
- `73122b5`: FOUND
- `8ce3748`: FOUND