feat(quick-260910-das): user-Dienst binden, Linie ziehen, Startsperre entschaerfen
- user.service.ts: findById/update/deactivate/delete bekommen einen Pflicht-Mandanten und laufen ueber forTenant(); create/update uebersetzen die plattformweite Eindeutigkeitsverletzung (P2002) in eine deutsche Konfliktmeldung ohne Halter/Mandant zu nennen; zwei neue Methoden (findAllForPlatformAdmin, findByIdForPlatformAdmin) bilden die Plattform-Administratorsicht als Schleife ueber alle Mandanten mit je einem gebundenen Lesezugriff nach; findByUsername bleibt bewusst ungebunden, Kopfkommentar richtiggestellt (Anmeldeweg laeuft seit Etappe 1 ueber SECURITY-DEFINER-Funktionen, kein Aufrufer mehr) - admin-seed.service.ts: Erstanlage-Pruefung bleibt ungebunden (mit Begruendung), Erstanlage des Administrators bindet an den unmittelbar zuvor angelegten Mandanten (Befund J-Korrektur); P2002 bei der Anlage wird wie "Administrator existiert bereits" behandelt statt den Start abzubrechen -- jeder andere Fehler bricht weiterhin ab - user.controller.ts: die vier Aufrufstellen der geaenderten Signaturen auf currentUser.tenantId umgestellt (Signatur-Minimalanpassung; die Rollenlogik inkl. Plattform-Administratorsicht folgt in Aufgabe 3) - Zwei-Klienten-Nachweis in beiden Testdateien (Muster groups.service.spec.ts), Falsifizierungsnachweis fuer beide Bereiche durchgefuehrt und zurueckgenommen (siehe SUMMARY) - docs/mandantentrennung-zugriffsklassifikation.md: Zwischenstand fuer (user.service.ts, user) und (admin-seed.service.ts, user) auf gemischt korrigiert, neue Zeile (user.service.ts, tenant) ergaenzt -- volle Klassenkorrektur mit Begruendung sowie die vier handgepflegten Uebersichtstabellen folgen in Aufgabe 3 - .planning/WINDOWS.md: offener Eintrag fuer die plattformweite Eindeutigkeit von username/email (Produktentscheidung fuer Etappe 3) - 802 Tests gruen (13 neue in user.service.spec.ts, 4 neue in admin-seed.service.spec.ts), Typpruefung sauber, Wegwerf-Werkzeug meldet weiterhin alle 53 Pruefungen bestanden Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
@@ -2,6 +2,7 @@ import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as argon2 from 'argon2';
|
||||
import { GroupsService } from '../groups/groups.service';
|
||||
import { forTenant } from '../prisma/prisma-tenant.extension';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
/**
|
||||
@@ -54,7 +55,21 @@ export class AdminSeedService implements OnApplicationBootstrap {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if admin already exists
|
||||
// Erstanlage-Pruefung beim Start (260910-das, Befund I/Aufgabe 2):
|
||||
// bleibt bewusst UNGEBUNDEN. HEUTE arbeitet sie richtig, weil zu diesem
|
||||
// Zeitpunkt noch kein Mandant existiert und `username` plattformweit
|
||||
// eindeutig ist -- eine gebundene Suche waere hier ohnehin nicht
|
||||
// formulierbar (es gibt noch keinen Mandanten, an den zu binden waere).
|
||||
// NACH DEM SCHARFSCHALTEN (RLS scharf, WINDOWS #18) liefert dieselbe
|
||||
// Abfrage fuer JEDEN Administrator `null`, weil ohne gesetzten
|
||||
// Mandantenkontext keine Zeile der Benutzertabelle sichtbar ist
|
||||
// (260910-das, Aufgabe 1, `user-ungebundene-suche-nach-benutzername-liefert-keine-zeile`).
|
||||
// Leere wird dann als Abwesenheit gedeutet, die natuerliche
|
||||
// Folgehandlung ist Anlegen (Schritt weiter unten laeuft), und das
|
||||
// Anlegen trifft die plattformweite Eindeutigkeit von `username` --
|
||||
// siehe die Entschaerfung direkt an der Erstanlage unten. Details:
|
||||
// docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt
|
||||
// "Bereich user", (u3) Punkt 1.
|
||||
const exists = await this.prisma.user.findUnique({
|
||||
where: { username },
|
||||
});
|
||||
@@ -64,7 +79,9 @@ export class AdminSeedService implements OnApplicationBootstrap {
|
||||
return;
|
||||
}
|
||||
|
||||
// Upsert default tenant
|
||||
// Upsert default tenant. `Tenant` traegt keinen Zeilenschutz (Aufgabe 1,
|
||||
// `tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar`) -- ungebunden lesen
|
||||
// und schreiben ist hier korrekt, nicht uebersehen.
|
||||
const tenant = await this.prisma.tenant.upsert({
|
||||
where: { slug: 'default' },
|
||||
update: {},
|
||||
@@ -77,19 +94,49 @@ export class AdminSeedService implements OnApplicationBootstrap {
|
||||
// repair below to backfill the membership afterwards.
|
||||
await this.groupsService.ensureDefaultGroup(tenant.id);
|
||||
|
||||
// Create Super-Admin user
|
||||
// Erstanlage des Administrators (260910-das, Befund J-Korrektur,
|
||||
// Aufgabe 2): GEBUNDEN an die Kennung des unmittelbar zuvor angelegten
|
||||
// bzw. geholten Mandanten. Die bisherige Klassifikationsbegruendung
|
||||
// ("es gibt strukturell keinen Mandanten zum Binden") war FALSCH -- der
|
||||
// Mandant ist an dieser Stelle bereits bekannt (`tenant.id` oben).
|
||||
// Ungebunden waere dieses Einfuegen nach dem Scharfschalten von der
|
||||
// Policy abgewiesen worden (Aufgabe 1,
|
||||
// `user-ungebundenes-einfuegen-abgelehnt`): eine FRISCHE Installation
|
||||
// haette ihren allerersten Administrator gar nicht anlegen koennen.
|
||||
const passwordHash = await argon2.hash(password);
|
||||
await this.prisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
email,
|
||||
passwordHash,
|
||||
role: 'SUPER_ADMIN',
|
||||
tenantId: tenant.id,
|
||||
mustChangePassword: forceChange,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
const tenantPrisma = forTenant(this.prisma, tenant.id) as any;
|
||||
try {
|
||||
await tenantPrisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
email,
|
||||
passwordHash,
|
||||
role: 'SUPER_ADMIN',
|
||||
tenantId: tenant.id,
|
||||
mustChangePassword: forceChange,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
} catch (err: any) {
|
||||
// Entschaerfung der Startsperre (260910-das, Befund I): trifft die
|
||||
// Erstanlage die plattformweite Eindeutigkeit von username/email
|
||||
// (P2002), bedeutet das an DIESER Stelle exakt dasselbe wie ein
|
||||
// Treffer der vorgeschalteten Pruefung oben ("Administrator existiert
|
||||
// bereits") -- die Pruefung hat ihn nur wegen der Unsichtbarkeit
|
||||
// nicht gefunden. Dieser eine Fehlerfall wird deshalb wie der bereits
|
||||
// vorhandene "existiert bereits"-Zweig behandelt: protokollieren,
|
||||
// NICHT abbrechen. Das ist KEINE Aufweichung der im Dateikopf
|
||||
// festgehaltenen Absicht (seedAdmin() bleibt bewusst ungekapselt) --
|
||||
// JEDER ANDERE Fehler bricht den Start weiterhin ab. Nur dieser eine,
|
||||
// an dieser Stelle gleichbedeutende Fall wird ergaenzt.
|
||||
if (err?.code === 'P2002') {
|
||||
this.logger.log(
|
||||
`Admin user "${username}" seed skipped: uniqueness collision on username/email (an administrator with this identity already exists, currently invisible under this tenant context) — see docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt "Bereich user"`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Admin user "${username}" seeded as SUPER_ADMIN in tenant "${tenant.slug}"`,
|
||||
@@ -105,6 +152,20 @@ export class AdminSeedService implements OnApplicationBootstrap {
|
||||
* own try/catch and is additionally wrapped as a whole, so a failing
|
||||
* tenant.findMany or a single tenant's ensureDefaultGroup call can never
|
||||
* block the API from starting.
|
||||
*
|
||||
* 260910-das, Befund K: dies ist der FUENFTE Fall der
|
||||
* Hintergrunddienst-Falle (docs/mandantentrennung-zugriffsklassifikation.md,
|
||||
* Abschnitt "Der Hintergrunddienst als Falle") und der bislang EINZIGE,
|
||||
* der auf BEIDEN Haelften bereits richtig ist -- uebergreifender Treiber
|
||||
* (`this.prisma.tenant.findMany`, UNGEBUNDEN, korrekt weil `Tenant`
|
||||
* keinen Zeilenschutz traegt), gebundener Rumpf
|
||||
* (`groupsService.ensureDefaultGroup(tenant.id)`, seit 260909-jts
|
||||
* vollstaendig ueber `forTenant()`/`withTenantTransaction()`). Bewusst
|
||||
* NICHT verschwiegen: der aeussere `try/catch` unten verschluckt jeden
|
||||
* Fehler des Treibers in eine Protokollzeile -- laeuft die Mandantenliste
|
||||
* nach dem Scharfschalten aus irgendeinem Grund leer, entsteht keine
|
||||
* Fehlermeldung, sondern gar keine Ausgabe. Die Reparatur meldet nur,
|
||||
* wenn sie etwas GETAN hat.
|
||||
*/
|
||||
private async ensureDefaultGroupsForAllTenants() {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user