---
phase: quick-260805-fok
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- apps/api/src/groups/groups.service.ts
- apps/api/src/groups/groups.service.spec.ts
- apps/api/src/tenant/tenant.service.ts
- apps/api/src/tenant/tenant.module.ts
- apps/api/src/tenant/tenant.service.spec.ts
- apps/api/src/user/admin-seed.service.ts
- apps/api/src/user/admin-seed.service.spec.ts
autonomous: true
requirements:
- 260805-fok
must_haves:
truths:
- "GroupsService.ensureDefaultGroup(tenantId) legt für einen Mandanten OHNE jede Gruppe die Standardgruppe 'Alle Benutzer' (isDefault: true) an, nimmt alle bestehenden Benutzer dieses Mandanten als MANUAL-Mitglieder auf und erzeugt Gruppen-Grants für alle aktuell aktiven Module — derselbe Endzustand, den die drei Backfill-INSERTs in 20260804130130_add_groups_and_module_grants pro Mandant herstellen."
- "Der Wächter ist 'null Gruppen im Mandanten'. Ein Mandant mit mindestens einer Gruppe wird NIE angefasst — auch dann nicht, wenn keine davon isDefault trägt. D-13 erlaubt dem Admin ausdrücklich, die Markierung abzuhängen oder umzuhängen; diese Entscheidung darf nicht überschrieben werden."
- "ensureDefaultGroup ist idempotent und race-sicher: ein zweiter Aufruf ist folgenlos, und eine P2002-Verletzung des partiellen Index Group_one_default_per_tenant durch einen parallelen Aufruf wird abgefangen statt propagiert."
- "TenantService.create ruft ensureDefaultGroup für den frisch angelegten Mandanten auf. Schlägt das fehl, wird der Fehler protokolliert und die Mandanten-Anlage kehrt trotzdem erfolgreich zurück (Muster aus UserService.create)."
- "AdminSeedService ruft ensureDefaultGroup direkt nach dem Tenant-Upsert auf, sodass die Standardgruppe existiert, BEVOR der Super-Admin über UserService.create angelegt wird und dort per addUserToDefaultGroup beitritt — genau die Reihenfolge, die auf dem Testserver gefehlt hat."
- "AdminSeedService führt am Ende von onApplicationBootstrap — nach dem Admin-Seed und unabhängig von dessen frühen Rückkehrpfaden (ENV fehlt / Admin existiert bereits) — die einmalige Reparatur über ALLE Mandanten aus. Sie läuft je Mandant in try/catch und kann den API-Start nie blockieren."
- "Jeder Schreibvorgang trägt eine explizite tenantId, die ausschließlich aus dem übergebenen Funktionsargument bzw. aus tenant.findMany stammt — nie aus Benutzereingaben."
- "Kein Schemawechsel und keine neue Migration: apps/api/prisma/schema.prisma bleibt unverändert, es bleiben genau 24 Migrationsverzeichnisse."
artifacts:
- "apps/api/src/groups/groups.service.ts — neue Methode ensureDefaultGroup(tenantId) mit Null-Gruppen-Wächter, Transaktion über Group + GroupMembership + ModuleGrant und P2002-Abfang."
- "apps/api/src/groups/groups.service.spec.ts — erweiterter In-Memory-Fake (group.count, user.findMany per tenantId, tenantModuleActivation, moduleGrant.createMany/findMany, $transaction mit Callback-Form) plus ensureDefaultGroup-Testblock."
- "apps/api/src/tenant/tenant.service.ts + tenant.module.ts — GroupsService injiziert, ensureDefaultGroup nach prisma.tenant.create, Logger für den nicht-fatalen Fehlerpfad."
- "apps/api/src/tenant/tenant.service.spec.ts — NEU: beweist den Aufruf und den nicht-fatalen Fehlerpfad."
- "apps/api/src/user/admin-seed.service.ts — seedAdmin() extrahiert, ensureDefaultGroup nach dem Tenant-Upsert, ensureDefaultGroupsForAllTenants() als abschließende Reparatur."
- "apps/api/src/user/admin-seed.service.spec.ts — NEU: Reihenfolge, Reparatur über alle Mandanten, Idempotenz über zwei Bootstrap-Läufe, Fehlerisolation je Mandant."
key_links:
- "ensureDefaultGroup <-> partieller Unique-Index Group_one_default_per_tenant (15-01): der Index ist der eigentliche Durchsetzungspunkt, der P2002-Abfang macht die Methode race-sicher."
- "AdminSeedService-Reihenfolge <-> sequenzieller await statt Verlass auf die Nest-Hook-Reihenfolge: GroupsModule ist Dependency von UserModule, ein eigener onApplicationBootstrap-Hook in GroupsModule liefe deshalb VOR dem Admin-Seed (dieselbe Falle, die tender-scheduler.service.ts ausführlich dokumentiert)."
- "ensureDefaultGroup <-> GroupsService.addUserToDefaultGroup (Z. 260): letztere kehrt still zurück, wenn keine markierte Standardgruppe existiert — deshalb muss die Gruppe vor jedem user.create stehen."
- "TenantModule -> GroupsModule Import: GroupsModule importiert selbst nichts, es entsteht keine Zirkularität (UserModule macht dasselbe bereits)."
---
Die Invariante „jeder Mandant hat eine Standardgruppe" gehört heute allein der einmaligen
Datenmigration 20260804130130_add_groups_and_module_grants. Auf einer frischen Installation
lief diese Migration gegen eine leere Datenbank, ihre drei Backfill-INSERTs trafen null Zeilen,
und der drei Sekunden später von AdminSeedService angelegte Mandant „Default" blieb ohne
Gruppe zurück (live nachgewiesen auf 192.168.13.12: tenants=1 users=4 groups=0 memberships=0
grants=0). Weil D-02 den Zugriff auf „geschlossen ohne Grant" umkehrt, sieht dort kein
einziger USER ein Modul, und die Berechtigungsmatrix hat keine Spalten.
Dieser Plan verlegt die Invariante dorthin, wo Mandanten entstehen, und repariert einmalig
die Installationen, die bereits ohne Gruppe dastehen.
Purpose: Eine frische Tessera-Installation ist ab dem ersten API-Start benutzbar, ohne dass
jemand von Hand eine Gruppe anlegen und als Standard markieren muss.
Output: ensureDefaultGroup(tenantId) auf GroupsService, aufgerufen aus beiden
Mandanten-Entstehungspfaden, plus eine Startup-Reparatur über alle Mandanten ohne jede Gruppe.
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
@.planning/STATE.md
@CLAUDE.md
# Achtung: die Stack-Tabelle in CLAUDE.md ist veraltet — real laufen Prisma 6.19.3 und Next.js 15.5.19.
# D-06 (Migration legt pro Mandant „Alle Benutzer" an), D-11/D-12 (jeder neue Benutzer wird
# Mitglied der Standardgruppe), D-13 (die Markierung entscheidet, nicht der Name — der Admin
# darf sie umhängen oder abschalten).
@.planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-CONTEXT.md
# Der Soll-Endzustand pro Mandant steht als SQL in den drei Backfill-INSERTs am Dateiende.
@apps/api/prisma/migrations/20260804130130_add_groups_and_module_grants/migration.sql
@apps/api/src/groups/groups.service.ts
@apps/api/src/groups/groups.service.spec.ts
@apps/api/src/user/admin-seed.service.ts
@apps/api/src/user/user.service.ts
@apps/api/src/tenant/tenant.service.ts
Task 1: GroupsService.ensureDefaultGroup(tenantId) + Spec
apps/api/src/groups/groups.service.ts, apps/api/src/groups/groups.service.spec.ts
- ensureDefaultGroup('t-neu') bei einem Mandanten ohne jede Gruppe legt genau eine Gruppe
namens 'Alle Benutzer' mit isDefault:true an und gibt sie zurück.
- Dabei werden alle Benutzer DIESES Mandanten als GroupMembership mit source MANUAL
aufgenommen; ein Benutzer eines fremden Mandanten wird nicht Mitglied.
- Für jede TenantModuleActivation des Mandanten mit isActive:true entsteht genau ein
ModuleGrant auf die neue Gruppe (tenantId gesetzt, groupId gesetzt, userId null);
eine Aktivierung mit isActive:false erzeugt keinen Grant, eine Aktivierung eines
fremden Mandanten ebenfalls nicht.
- Ein Mandant, der mindestens eine Gruppe hat, aber KEINE davon mit isDefault:true, wird
nicht angefasst: Rückgabe null, keine neue Gruppe, keine neue Mitgliedschaft, kein
neuer Grant (D-13 — die abgehängte Markierung ist eine Admin-Entscheidung).
- Ein Mandant, der bereits eine Gruppe mit isDefault:true hat, wird ebenfalls nicht
angefasst: Rückgabe null, Gruppenzahl unverändert.
- Zwei aufeinanderfolgende Aufrufe für denselben Mandanten hinterlassen genau eine Gruppe
und genau eine Mitgliedschaft pro Benutzer (Idempotenz über Neustarts hinweg).
- Wirft group.create einen P2002 (paralleler Aufruf gewinnt das Rennen gegen den partiellen
Index Group_one_default_per_tenant), wirft ensureDefaultGroup nicht, sondern liefert null.
- Ein Mandant ganz ohne Benutzer und ohne aktive Module bekommt trotzdem die leere
Standardgruppe.
Ergänze GroupsService (apps/api/src/groups/groups.service.ts) um eine öffentliche Methode ensureDefaultGroup(tenantId: string), platziert direkt vor addUserToDefaultGroup, damit die beiden Standardgruppen-Methoden beieinanderstehen. Rückgabetyp: das angelegte Group-Objekt, oder null, wenn nichts zu tun war.
Ablauf: Zuerst prisma.group.count mit where { tenantId }. Ist das Ergebnis groesser als 0, gib sofort null zurueck — der Wächter prüft AUSSCHLIESSLICH auf das Fehlen jeglicher Gruppe, niemals auf das Fehlen der Markierung. Begründe das im Doc-Kommentar mit D-13: ein Mandant, dessen Admin die Markierung abgehängt oder auf eine andere Gruppe umgehängt hat, hat Gruppen — ihn hier nachträglich zu bedienen würde diese Entscheidung überschreiben.
Ist die Gruppenzahl 0, führe die drei Schritte in einer interaktiven Transaktion aus (this.prisma.$transaction mit async-Callback und tx-Client — die Array-Form aus update() reicht nicht, weil Schritt 2 und 3 die id der in Schritt 1 erzeugten Gruppe brauchen). Reihenfolge und Semantik spiegeln die drei Backfill-INSERTs der Migration:
1. tx.group.create mit data { tenantId, name: 'Alle Benutzer', isDefault: true }. Der Name ist zeichengleich zu dem in der Migration, damit reparierte und migrierte Installationen dieselbe Gruppe zeigen.
2. tx.user.findMany mit where { tenantId } und select { id: true }; bei mindestens einem Treffer tx.groupMembership.createMany mit einem Eintrag je Benutzer (groupId der neuen Gruppe, userId, source: MembershipSource.MANUAL) und skipDuplicates: true. Das entspricht D-11/D-12 — dieselbe Herkunft, die addUserToDefaultGroup setzt.
3. tx.tenantModuleActivation.findMany mit where { tenantId, isActive: true } und select { moduleId: true }; bei mindestens einem Treffer tx.moduleGrant.createMany mit einem Eintrag je Modul (tenantId, moduleId, groupId der neuen Gruppe, userId: null) und skipDuplicates: true. Das entspricht D-06.
Umschliesse den gesamten $transaction-Aufruf mit try/catch. Fange ausschliesslich err.code === 'P2002' ab und gib in diesem Fall null zurueck; alles andere wird weitergeworfen. Erklaere im Kommentar, dass der partielle Index Group_one_default_per_tenant aus 15-01 der eigentliche Durchsetzungspunkt bleibt und dieser Abfang nur den Fall abdeckt, dass zwei gleichzeitige Aufrufe (Startup-Reparatur und Mandanten-Anlage) beide eine Gruppenzahl von 0 gesehen haben.
Jede where- und data-Angabe traegt die uebergebene tenantId explizit; die Methode leitet sie nirgends aus einem gelesenen Datensatz oder einer Benutzereingabe ab. Bleib bei this.prisma mit expliziten Mandantenfiltern, wie der Rest der Datei — kein forTenant, keine RLS-Session-Variable.
Erweitere in apps/api/src/groups/groups.service.spec.ts den vorhandenen makeFakePrisma-Fake, ohne bestehende Tests zu brechen:
- group.count hinzufuegen: zaehlt die Gruppen mit passender tenantId.
- user.findMany so umbauen, dass es sowohl die bisherige Form (where.id.in kombiniert mit where.tenantId, genutzt von addMembers) als auch die neue Form (nur where.tenantId) bedient — where.id darf undefined sein, ohne dass der Zugriff auf .in fehlschlaegt.
- tenantModuleActivation.findMany ergaenzen (filtert ueber eine neue Map nach tenantId und, falls angegeben, isActive) plus einen __seedActivation-Helfer im Stil von __seedUser/__seedGrant.
- moduleGrant um findMany (filtert nach tenantId und/oder groupId) und createMany (mit skipDuplicates auf der Kombination tenantId+moduleId+groupId) erweitern; count bleibt wie es ist.
- group.create um eine zusaetzliche Pruefung ergaenzen, die einen P2002 wirft, wenn bereits eine Gruppe desselben Mandanten mit isDefault:true existiert und die neue Zeile ebenfalls isDefault:true traegt — damit bildet der Fake den partiellen Index Group_one_default_per_tenant nach.
- $transaction so erweitern, dass es beide Formen akzeptiert: bei einem Array wie bisher Promise.all, bei einer Funktion den Aufruf dieser Funktion mit dem Fake selbst als tx-Client.
Fuege danach am Dateiende einen Testblock fuer ensureDefaultGroup an, der jeden Punkt des behavior-Blocks abdeckt. Nutze fuer den P2002-Fall einen Mandanten, dessen isDefault-Gruppe direkt im Fake vorbelegt wird, waehrend group.count auf 0 gestellt ist (z.B. per Monkey-Patch von prisma.group.count auf eine Funktion, die 0 liefert) — so laesst sich das verlorene Rennen ohne echte Nebenlaeufigkeit nachstellen.
pnpm --filter @tessera/api exec vitest run src/groups/groups.service.spec.ts
pnpm --filter @tessera/api run type-check
Alle Specs in groups.service.spec.ts sind gruen, inklusive der neuen ensureDefaultGroup-Tests fuer Anlage, Mitglieder, Grants, den Null-Gruppen-Waechter, den Nicht-Anfassen-Fall bei vorhandenen Gruppen ohne Markierung, Idempotenz und den P2002-Abfang. type-check ist sauber.
Task 2: Beide Mandanten-Entstehungspfade verdrahten + Startup-Reparatur + Specs
apps/api/src/tenant/tenant.service.ts, apps/api/src/tenant/tenant.module.ts, apps/api/src/tenant/tenant.service.spec.ts, apps/api/src/user/admin-seed.service.ts, apps/api/src/user/admin-seed.service.spec.ts
- TenantService.create legt den Mandanten an und ruft danach genau einmal
groupsService.ensureDefaultGroup mit der id des neu angelegten Mandanten auf; der
Rueckgabewert bleibt der Mandant.
- Wirft ensureDefaultGroup, liefert TenantService.create den Mandanten trotzdem zurueck
und wirft nicht (Fehler wird protokolliert).
- AdminSeedService.onApplicationBootstrap ruft auf einer frischen Installation in genau
dieser Reihenfolge auf: tenant.upsert, dann ensureDefaultGroup mit der id des Mandanten,
erst danach user.create fuer den Super-Admin.
- Fehlen die ENV-Variablen TESSERA_ADMIN_USER/EMAIL/PASSWORD, wird kein Benutzer angelegt,
die Reparatur ueber alle Mandanten laeuft aber trotzdem.
- Existiert der Admin-Benutzer bereits (der Fall auf dem Testserver), wird kein Benutzer
angelegt, die Reparatur ueber alle Mandanten laeuft aber trotzdem.
- Die Reparatur ruft ensureDefaultGroup fuer JEDEN von prisma.tenant.findMany gelieferten
Mandanten auf, auch fuer solche, die nicht 'default' heissen.
- Wirft ensureDefaultGroup fuer einen Mandanten, werden die uebrigen Mandanten trotzdem
abgearbeitet und onApplicationBootstrap wirft nicht.
- Ein zweiter onApplicationBootstrap-Lauf (Neustart) ruft ensureDefaultGroup erneut auf,
loest aber keinen zusaetzlichen user.create aus.
apps/api/src/tenant/tenant.service.ts: Injiziere GroupsService zusaetzlich zu PrismaService und lege einen Logger im Stil von UserService an (private readonly logger = new Logger(TenantService.name)). In create() das Ergebnis von prisma.tenant.create in eine Konstante nehmen, danach ensureDefaultGroup mit der id des neuen Mandanten in einem try/catch aufrufen, im catch mit logger.error protokollieren und anschliessend den Mandanten zurueckgeben. Formuliere den Doc-Kommentar auf Deutsch wie in GroupsService/UserService und halte fest, warum der Fehler nicht propagiert wird: die Mandanten-Anlage selbst ist erfolgreich, und die Startup-Reparatur holt eine gescheiterte Gruppenanlage beim naechsten API-Start nach.
apps/api/src/tenant/tenant.module.ts: GroupsModule importieren. Vermerke im Kommentar, dass GroupsModule selbst nichts importiert und deshalb keine Zirkularitaet entsteht — UserModule bindet GroupsModule bereits nach demselben Muster ein.
apps/api/src/user/admin-seed.service.ts: Injiziere GroupsService zusaetzlich zu PrismaService und ConfigService. Verschiebe den kompletten bisherigen Rumpf von onApplicationBootstrap unveraendert in eine neue private Methode seedAdmin(), inklusive beider frueher Rueckkehrpfade. onApplicationBootstrap besteht danach aus zwei sequenziellen await-Aufrufen: erst seedAdmin(), dann die neue private Methode ensureDefaultGroupsForAllTenants(). Genau diese sequenzielle Reihenfolge ist der Ordering-Garant — begruende im Klassen-Doc-Kommentar, warum die Reparatur NICHT als eigener onApplicationBootstrap-Hook in GroupsModule sitzt: GroupsModule ist eine Dependency von UserModule, seine Hooks laufen deshalb frueher, und die Reparatur wuerde vor dem Anlegen des Default-Mandanten greifen. Verweise dabei auf die ausfuehrlich dokumentierte Variante desselben Problems in tenders/tender-scheduler.service.ts.
In seedAdmin() unmittelbar nach dem tenant.upsert und VOR dem user.create ein await auf ensureDefaultGroup mit tenant.id einfuegen. Damit existiert die markierte Standardgruppe, bevor UserService.create laeuft, und der Super-Admin wird ueber den normalen addUserToDefaultGroup-Pfad Mitglied statt ueber die Reparatur.
ensureDefaultGroupsForAllTenants() implementieren: prisma.tenant.findMany mit select { id: true, slug: true }, dann eine Schleife, die je Mandant ensureDefaultGroup in einem eigenen try/catch aufruft und die Anzahl der tatsaechlich erzeugten Gruppen mitzaehlt (Rueckgabe ungleich null). Ein Fehler bei einem Mandanten wird per logger.error mit slug protokolliert und beendet die Schleife nicht. Wurde mindestens eine Gruppe erzeugt, eine zusammenfassende logger.warn-Zeile mit der Anzahl schreiben, sonst eine logger.log-Zeile auf debug-artigem Niveau oder gar keine. Die gesamte Methode zusaetzlich in ein aeusseres try/catch legen, damit auch ein fehlgeschlagenes tenant.findMany nur protokolliert wird und den API-Start nicht abbricht. Die Log-Texte in dieser Datei bleiben englisch, wie die vorhandenen Zeilen. seedAdmin() bleibt bewusst UNgekapselt: schlaegt der Admin-Seed fehl, soll der Start weiterhin laut scheitern — das ist bestehendes Verhalten und wird hier nicht aufgeweicht.
apps/api/src/tenant/tenant.service.spec.ts (NEU): Vitest-Spec im Stil von user.service.spec.ts — vi.fn-Mocks fuer prisma.tenant.create und fuer groupsService.ensureDefaultGroup, Service per new TenantService(prisma, groupsService) instanziiert. Deckt die beiden TenantService-Punkte des behavior-Blocks ab.
apps/api/src/user/admin-seed.service.spec.ts (NEU): Vitest-Spec mit vi.fn-Mocks fuer prisma.tenant.upsert, prisma.tenant.findMany, prisma.user.findUnique, prisma.user.create, fuer configService.get (liefert die ENV-Werte je Testfall) und fuer groupsService.ensureDefaultGroup. Service per new AdminSeedService(prisma, configService, groupsService) instanziiert — Konstruktor-Reihenfolge an die Implementierung angleichen. Deckt die sechs AdminSeedService-Punkte des behavior-Blocks ab. Die Reihenfolgepruefung ueber die vi.fn-Aufrufreihenfolge fuehren (z.B. mock.invocationCallOrder oder ein gemeinsames Aufruf-Log-Array, in das jeder Mock seinen Namen schiebt), nicht ueber blosse Aufrufzaehler.
pnpm --filter @tessera/api exec vitest run src/tenant/tenant.service.spec.ts src/user/admin-seed.service.spec.ts
pnpm --filter @tessera/api run test
pnpm --filter @tessera/api run type-check
test "$(ls -1d apps/api/prisma/migrations/*/ | wc -l)" -eq 24 && git diff --quiet -- apps/api/prisma/schema.prisma && echo schema-unveraendert
grep -q 'ensureDefaultGroup' apps/api/src/tenant/tenant.service.ts && grep -q 'ensureDefaultGroup' apps/api/src/user/admin-seed.service.ts && grep -q 'GroupsModule' apps/api/src/tenant/tenant.module.ts && echo verdrahtung-ok
Beide neuen Specs sind gruen, die komplette API-Suite laeuft durch und type-check ist sauber. TenantService.create und AdminSeedService rufen beide ensureDefaultGroup auf, AdminSeedService fuehrt die Reparatur ueber alle Mandanten unabhaengig von seinen fruehen Rueckkehrpfaden aus, und weder schema.prisma noch die Migrationsverzeichnisse wurden angefasst.
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Admin-API -> TenantService.create | SUPER_ADMIN legt einen Mandanten an; name/slug sind Benutzereingabe, die entstehende tenantId ist es nicht |
| Startup-Prozess -> Datenbank | Die Reparatur schreibt unbeaufsichtigt in JEDEN Mandanten, ohne Request-Kontext und ohne JWT |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-fok-01 | Elevation of Privilege | GroupsService.ensureDefaultGroup | high | mitigate | tenantId kommt ausschliesslich aus dem Funktionsargument (TenantService: id des soeben erzeugten Mandanten; Reparatur: id aus tenant.findMany). Weder Gruppen-, Mitgliedschafts- noch Grant-Schreibvorgang leitet die tenantId aus einem gelesenen Fremdobjekt oder aus Request-Daten ab. |
| T-fok-02 | Elevation of Privilege | Mitglieder-Backfill in ensureDefaultGroup | high | mitigate | user.findMany filtert auf where { tenantId }; ein Benutzer eines fremden Mandanten kann nie Mitglied werden. Spec-Fall im behavior-Block deckt das explizit ab. |
| T-fok-03 | Elevation of Privilege | Grant-Backfill in ensureDefaultGroup | high | mitigate | tenantModuleActivation.findMany filtert auf where { tenantId, isActive: true }; ein Grant entsteht nur fuer ein Modul, das fuer genau diesen Mandanten aktiv ist — nie fuer ein bloss katalogisiertes oder fuer einen fremden Mandanten aktiviertes Modul. |
| T-fok-04 | Tampering | Startup-Reparatur vs. D-13 | medium | mitigate | Der Waechter prueft auf null Gruppen, nicht auf die fehlende Markierung. Ein Admin, der die Standardmarkierung bewusst abgehaengt oder umgehaengt hat, wird von der Reparatur nicht ueberstimmt. |
| T-fok-05 | Denial of Service | onApplicationBootstrap | medium | mitigate | Die Reparatur laeuft je Mandant in try/catch und zusaetzlich als Ganzes gekapselt; ein Datenbankfehler wird protokolliert und blockiert den API-Start nicht. |
| T-fok-06 | Tampering | Nebenlaeufige Gruppenanlage | low | accept | Zwei gleichzeitige Aufrufe koennen beide die Gruppenzahl 0 lesen; der partielle Unique-Index Group_one_default_per_tenant laesst nur einen gewinnen, der Verlierer faengt P2002 ab und liefert null. Kein weiterer Sperrmechanismus noetig. |
- `pnpm --filter @tessera/api run test` — komplette API-Suite gruen (Ausgangsstand: 387 Tests aus Phase 14 plus die Phase-15-Specs; keine bestehende Spec darf brechen).
- `pnpm --filter @tessera/api run type-check` — sauber.
- `git diff --stat` zeigt ausschliesslich die sieben in files_modified gelisteten Dateien; insbesondere keine Aenderung an apps/api/prisma/schema.prisma und kein neues Verzeichnis unter apps/api/prisma/migrations/.
- Kein Deploy auf den Testserver aus diesem Plan heraus — das uebernimmt der Benutzer. Der erwartete Effekt dort nach dem naechsten Container-Start: der Mandant „Default" bekommt die Gruppe „Alle Benutzer" mit isDefault:true, die vier vorhandenen Benutzer werden Mitglieder, und fuer jedes aktive Modul entsteht ein Gruppen-Grant.
- Ein ueber POST /tenants angelegter Mandant hat unmittelbar danach genau eine Gruppe, und diese traegt isDefault:true.
- Eine frische Installation (leere Datenbank, erster Container-Start) hat nach dem API-Start eine Standardgruppe im Mandanten „Default", und der geseedete Super-Admin ist deren Mitglied.
- Eine bestehende Installation mit Mandanten ohne jede Gruppe erhaelt diese beim naechsten API-Start nachtraeglich, samt Mitgliedschaften aller Bestandsbenutzer und Grants aller aktiven Module.
- Ein Mandant, dessen Gruppen existieren, aber keine davon als Standard markiert ist, bleibt beim Neustart unveraendert.
- Zwei aufeinanderfolgende API-Starts erzeugen nicht zwei Gruppen und keine doppelten Mitgliedschaften.
- Keine Schemaaenderung, keine neue Migration, kein `prisma db push`.