diff --git a/.planning/quick/260911-fh9-mandantentrennung-etappe-2-bereich-auth-/260911-fh9-PLAN.md b/.planning/quick/260911-fh9-mandantentrennung-etappe-2-bereich-auth-/260911-fh9-PLAN.md
new file mode 100644
index 0000000..b6506e7
--- /dev/null
+++ b/.planning/quick/260911-fh9-mandantentrennung-etappe-2-bereich-auth-/260911-fh9-PLAN.md
@@ -0,0 +1,1074 @@
+---
+phase: quick-260911-fh9
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+autonomous: true
+requirements: [WINDOWS-18, ETAPPE-2-AUTH]
+
+files_modified:
+ - apps/api/scripts/rls-scratch-check.mjs
+ - docs/mandantentrennung-etappe2-fehlerrichtung.md
+ - apps/api/src/auth/auth.service.ts
+ - apps/api/src/auth/auth.service.spec.ts
+ - apps/api/src/auth/auth.controller.ts
+ - apps/api/src/auth/auth.module.ts
+ - apps/api/src/auth/auth.controller.spec.ts
+ - docs/mandantentrennung-zugriffsklassifikation.md
+ - .planning/WINDOWS.md
+
+estimate:
+ tokens: 180000
+ raw_tokens: 180000
+ tasks: 3
+ confidence: low
+
+must_haves:
+ truths:
+ - "Die Grenze zwischen Anmeldeweg und Nach-Anmeldung ist GELESEN und GEMESSEN, nicht angenommen: `validateUser`, `requestPasswordReset`, `resetPassword` suchen VOR bekanntem Mandanten ueber die drei SECURITY-DEFINER-Funktionen auf dem UNGEBUNDENEN Klienten und bleiben es (drei `$queryRaw`-Stellen, Migration unangetastet, `pg_proc` bestaetigt SECURITY DEFINER, STABLE, festen Suchpfad und LIMIT 1 in der Wegwerf-Datenbank); `getMe`, `changePassword`, `adminResetPassword` laufen NACH der Anmeldung, der Mandant steht im signierten Sitzungsnachweis, und genau diese drei werden gebunden."
+ - "Der Mandant der drei gebundenen Methoden stammt ausschliesslich aus dem Sitzungsnachweis (`@CurrentUser().tenantId`, das von `login()` aus der Funktionszeile signierte Claim) — NICHT aus `req.tenantId` (das fuer SUPER_ADMIN per `x-tenant-id` umschaltbar ist und den Anfragenden sich selbst gegenueber unsichtbar machen wuerde), NICHT aus Pfad, Rumpf oder Kopfzeile. Fuer SUPER_ADMIN bei `adminResetPassword` kommt der Mandant des ZIELS aus dem gebundenen Fan-out `UserService.findByIdForPlatformAdmin` (Praezedenzfall `user.controller.ts`, 260910-das). Gates zaehlen `req.tenantId` und `x-tenant-id` in `auth.controller.ts` auf null."
+ - "Die Rechteausweitung ueber die Mandantengrenze ist geschlossen und gemessen: ein ADMIN von Mandant A, der `POST /auth/admin-reset-password/:userId` fuer einen Benutzer von Mandant B aufruft, findet unter dem gebundenen Klienten keine Zeile (Pruefung 6/9 im Werkzeug, ueber den GENERIERTEN Client) und bekommt die bestehende 400-Meldung ohne Aussage ueber den fremden Mandanten; als Testfall festgenagelt und durch Rueckbau falsifiziert."
+ - "Die Rechteausweitung INNERHALB des Mandanten ist in diesem Handler geschlossen (ein ADMIN setzt das Kennwort eines SUPER_ADMIN nicht mehr — `ForbiddenException`, Testfall) und fuer den Schwesterweg `PATCH /users/:id` (ausserhalb der Erlaubnisliste) gemessen, in (h4) festgehalten und als OFFENER Ledger-Eintrag mit konkreter Reparatur uebergeben."
+ - "Die umgekehrte Fehlerrichtung ist je Methode in ihrer tatsaechlichen Auspraegung benannt und gemessen: `getMe` liefert `null` — der Controller antwortet 200 mit leerem Rumpf, `fetchCurrentUser` macht daraus `null`, `header.tsx` und `account-settings-form.tsx` verschlucken das in eine Portalhuelle OHNE angemeldeten Benutzer (kein Name, keine Admin-Navigation) — NICHT laut, sondern die Familie von WINDOWS #26; `changePassword` liest sich im Frontend als `networkError` (nicht als falsches Kennwort — `auth-actions.ts` uebersetzt nur die eine Meldung); `adminResetPassword` als `User not found` ohne UI-Aufrufer. Steht in (h2)/(h3), als Ledger-Eintrag, mit Etappe-4-Vorabpruefung."
+ - "Die Testlage traegt: `auth.service.spec.ts` hat KEINE Identitaets-Attrappe mehr, sondern zwei unterscheidbare Klienten (`__makeBoundClient`), der ungebundene Nachbau hat KEIN Benutzermodell und der gebundene KEIN `$queryRaw` — beide Grenzen falsifizierbar; `auth.controller.spec.ts` existiert neu und nagelt Mandantenquelle, Rollenverzweigung, Rollen- und Public-Metadaten fest."
+ - "Etappe 3 wird durch diesen Plan nicht schwerer: die Bindung haengt am Claim `tenantId` und an `User.id` (plattformweite UUID, Kette aus 260911-cwh), nicht an `username`/`email`; was Etappe 3 am Anmeldeweg umbauen muss (Mandant VOR der Suche, Funktionen mit zwei Gleichheitsbedingungen — ENGER, nicht weiter), steht in (h4)(a) und im Klassifikationsdokument."
+ - "Alle fuenf handgepflegten Dokumentstellen der Klassifikation sind nachgezogen und DERIVIERT gegatet (Uebersichtszeile aus der Messanweisung, Summenzeile, Bestandsaufnahme-Zeile, Klassen-Verteilung mit Stand-Vermerk, Hintergrunddienst-Abschnitt ohne sechsten Fall, `Was diese Etappe NICHT entscheidet` mit dem Etappe-3-Punkt); der Umfang ist als ERLAUBNISLISTE gegen `6236b30` gegatet."
+ - "Baseline gehalten am Ende JEDER Aufgabe: mindestens 911 Tests gruen (nach Aufgabe 2 und 3 mehr), Typpruefung sauber, das Wegwerf-Werkzeug meldet alle Pruefungen bestanden (mindestens 120). Der Schalter bleibt AUS, Schema und Migrationen unveraendert, keine Compose- oder Umgebungsdatei angefasst, nichts in Active Directory, die drei Anmeldefunktionen unangetastet."
+ artifacts:
+ - "apps/api/scripts/rls-scratch-check.mjs — ein dreizehnter Abschnitt `runAuthAreaChecks` (getrennt von `runAuthLookupChecks`) mit mindestens zehn namentlich benannten Pruefungen, davon mindestens sechs ueber den GENERIERTEN Client an der auf den vollen Spaltensatz gebrachten Wegwerf-Tabelle `User` (Vergleich zur Laufzeit gegen die SKALAREN Felder von `model User` — neuer Helfer, der Relationsfelder ueber ihren Typ ausschliesst); `pg_proc`-Messung der drei Funktionen"
+ - "docs/mandantentrennung-etappe2-fehlerrichtung.md — Abschnitt `## Bereich auth` unmittelbar VOR `## Verweis` mit (h1) Messung, (h2) Signaltabelle je Pfad (Anmeldeweg UND Nach-Anmeldung), (h3) Leere als Abwesenheit im Backend UND Frontend, (h4) bewusst nicht geloest (darunter Etappe 3, Schwesterweg, Frontend), (h5) bewusst nicht angefasst"
+ - "apps/api/src/auth/auth.service.ts — `getMe(tenantId, userId)`, `changePassword(tenantId, userId, ...)`, `adminResetPassword(tenantId, callerRole, userId, ...)` je mit EINEM Klienten `tenantPrisma`; die drei `$queryRaw`-Anmeldesuchen unveraendert auf dem ungebundenen Klienten; Kopfkommentare nennen Grenze, Quelle des Mandanten und Etappe-3-Vorbehalt"
+ - "apps/api/src/auth/auth.service.spec.ts — Zwei-Klienten-Nachbau, alle bestehenden Faelle umgestellt, neue Faelle fuer die drei Methoden, Grenz-Test (Anmeldesuche ungebunden, Schreibzugriff gebunden), Wachhund"
+ - "apps/api/src/auth/auth.controller.ts — `me`/`changePassword` reichen `user.tenantId` aus dem Sitzungsnachweis durch; `adminResetPassword` verzweigt nach Rolle (ADMIN: eigener Mandant; SUPER_ADMIN: Fan-out ueber `UserService.findByIdForPlatformAdmin`), Kopfkommentar nennt Grund und Praezedenzfall"
+ - "apps/api/src/auth/auth.module.ts — importiert `UserModule` (zyklusfrei, gemessen)"
+ - "apps/api/src/auth/auth.controller.spec.ts — NEU: Mandantenquelle je Handler, Rollenverzweigung, unbekanntes Ziel, Rollen-Metadaten (`ROLES_KEY`) und Public-Metadaten (`IS_PUBLIC_KEY`) fuer alle sieben Handler"
+ - "docs/mandantentrennung-zugriffsklassifikation.md — Uebersichtszeile `auth` mit neu gemessenen Zahlen, Summenzeile, Bestandsaufnahme-Zeile `auth.service.ts`/`user` auf `gebunden`, Klassen-Verteilung mit Stand-Vermerk (unveraendert, ausdruecklich), Hintergrunddienst-Abschnitt (kein sechster Fall, gemessen), `Was diese Etappe NICHT entscheidet` mit dem Etappe-3-Anmeldeweg-Punkt"
+ - ".planning/WINDOWS.md — zwei neue OFFENE Eintraege ueber `gsd-tools windows append`: die verschluckte Leere im Frontend (Familie #23/#25/#26) und die Rechteausweitung ADMIN -> SUPER_ADMIN im Schwesterweg `PATCH /users/:id`"
+ key_links:
+ - "`JwtStrategy.validate` liefert `{ id: payload.sub, username, role, tenantId }`; `login()` signiert `tenantId: user.tenantId` aus der Zeile von `auth_lookup_user_by_username`; `@CurrentUser()` gibt genau dieses Objekt zurueck — das ist die einzige Mandantenquelle der drei Methoden. `TenantGuard` setzt daneben `req.tenantId` (fuer SUPER_ADMIN per `x-tenant-id` umschaltbar, vier Sender im Marktplatz-Frontend) — fuer Selbstbedienung UNGEEIGNET, weil die eigene Zeile im eigenen Mandanten liegt."
+ - "`UserService.findByIdForPlatformAdmin(id)` (user.service.ts) liest `Tenant` ungebunden (keine Regel, gemessen 260911-e2s) und sucht je Mandant gebunden — liefert die Zeile samt `tenantId`; `AuthModule` importiert dafuer `UserModule` (UserModule importiert GroupsModule, GroupsModule importiert nichts, kein Modul ausser AppModule importiert AuthModule — zyklusfrei, gemessen)."
+ - "Die drei Funktionen (20260909160000) sind SECURITY DEFINER mit festem Spaltensatz und LIMIT 1; ihre `$queryRaw`-Aufrufe MUESSEN auf `this.prisma` bleiben — ein gebundener `$queryRaw` liefe durch `$allOperations` mit gesetztem Kontext (fuer SECURITY DEFINER wirkungslos, aber ein falsches Signal fuer jeden Leser). Der Nachbau im Test hat auf dem gebundenen Klienten KEIN `$queryRaw`."
+ - "`getMe` liefert `null` -> NestJS `ExpressAdapter.reply` sendet bei `isNil(body)` einen leeren Rumpf mit 200 -> `fetchCurrentUser` (`auth-actions.ts`) laeuft in `response.json()` auf den leeren Rumpf, faengt und liefert `null` -> `header.tsx` `if (u)` und `account-settings-form.tsx` `if (u)` tun nichts. Zur Ausfuehrungszeit an allen vier Gliedern nachzulesen."
+ - "Die Wegwerf-Tabelle `User` des Werkzeugs hat heute 10 der 15 skalaren Spalten (fehlend: createdAt, updatedAt, lastLoginAt, avatarPath, accentColor); `findUnique` ohne `select` (die Form von `changePassword`/`adminResetPassword`) und das `select` von `getMe` (nennt avatarPath/accentColor) scheitern auf dem generierten Client mit P2022, solange die Spalten fehlen — die dashboard-Lehre (260910-krx, Pruefung 5b)."
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
+Etappe 2 der Mandantentrennung, elfter Bereich: `auth`. Fuenf ungebundene
+Zugriffe auf das Benutzermodell in `auth.service.ts`, verteilt auf drei
+Methoden, die in Etappe 1 (260909-eor) BEWUSST liegen gelassen wurden: `getMe`,
+`changePassword`, `adminResetPassword`. Alle drei laufen nach der Anmeldung,
+der Mandant steht im signierten Sitzungsnachweis — aber keine der drei nimmt
+ihn heute entgegen. Binden heisst hier: Signaturen aendern, jeden Aufrufer in
+`auth.controller.ts` umstellen, und dabei entscheiden, WOHER der Mandant
+kommt.
+
+Zweck: dieser Bereich traegt die Grenze, an der die ganze Mandantentrennung
+haengt. Der Anmeldeweg selbst (Benutzer suchen, BEVOR der Mandant bekannt ist)
+wurde in Etappe 1 ueber drei enge SECURITY-DEFINER-Funktionen geloest, und
+nichts in diesem Plan darf diese Anordnung anfassen — die Grenze zwischen
+"vor der Anmeldung, auf den Funktionen" und "nach der Anmeldung, gebunden"
+wird hier gelesen, gemessen und festgenagelt, nicht angenommen. Der zweite
+Grund: `adminResetPassword` ist ein Administrator, der auf einen ANDEREN
+Benutzer wirkt, und prueft heute nicht, ob dieser Benutzer im eigenen
+Mandanten liegt. Mit einem Mandanten ist das harmlos; mit zweien ist es
+Rechteausweitung ueber die Mandantengrenze. Die Bindung an den Mandanten aus
+dem Sitzungsnachweis schliesst das — sofern der Mandant wirklich aus der
+Sitzung kommt und nicht aus etwas, das der Administrator selbst schicken
+koennte.
+
+Ueber diesem Bereich steht die Etappe-3-Entscheidung: Anmeldenamen werden je
+Mandant eindeutig. Dann muss der Anmeldeweg den Mandanten VOR der Suche
+kennen — ein Umbau der Funktionsanordnung, NICHT dieser Auftrag. Dieser Plan
+darf nichts entscheiden, was das schwerer macht, und muss sagen, was Etappe 3
+hier wieder aufmachen wird.
+
+Ergebnis: eine gemessene Kritikschrift, drei gebundene Methoden mit
+geaenderten Signaturen, ein Controller, der den Mandanten ausschliesslich aus
+dem Sitzungsnachweis nimmt (und fuer die oberste Rolle aus dem gebundenen
+Fan-out), eine Testdatei ohne Identitaets-Attrappe und eine neue fuer den
+Controller, Dokumente, die am Ende nachweislich mit dem Quelltext
+uebereinstimmen, und zwei Ledger-Eintraege fuer das, was gemessen, aber
+bewusst nicht in diesem Bereich behoben wird.
+
+**Der Schalter bleibt AUS.** `DATABASE_URL` zeigt weiterhin auf die Rolle
+`tessera` mit `BYPASSRLS`. Schema und Migrationen werden NICHT angefasst, die
+drei Anmeldefunktionen NICHT. Nichts wird in Active Directory geaendert.
+
+
+
+@~/.claude/gsd-core/workflows/execute-plan.md
+@~/.claude/gsd-core/templates/summary.md
+
+
+
+@.planning/STATE.md
+@docs/mandantentrennung-zugriffsklassifikation.md
+@docs/mandantentrennung-etappe2-fehlerrichtung.md
+@docs/mandantentrennung-datenbankrolle.md
+@apps/api/prisma/migrations/20260909160000_auth_lookup_functions/migration.sql
+@apps/api/prisma/migrations/20260618112124_auth_multi_tenancy/migration.sql
+@apps/api/prisma/migrations/20260618112133_rls_policies/migration.sql
+@apps/api/prisma/migrations/20260630095533_add_user_avatar/migration.sql
+@apps/api/prisma/migrations/20260702000000_add_user_accent_color/migration.sql
+@apps/api/src/prisma/prisma-tenant.extension.ts
+@apps/api/src/prisma/rls-access-inventory.spec.ts
+@apps/api/scripts/rls-scratch-check.mjs
+@apps/api/src/auth/auth.service.ts
+@apps/api/src/auth/auth.service.spec.ts
+@apps/api/src/auth/auth.controller.ts
+@apps/api/src/auth/auth.module.ts
+@apps/api/src/auth/strategies/jwt.strategy.ts
+@apps/api/src/auth/strategies/local.strategy.ts
+@apps/api/src/auth/decorators/current-user.decorator.ts
+@apps/api/src/auth/decorators/public.decorator.ts
+@apps/api/src/auth/decorators/roles.decorator.ts
+@apps/api/src/auth/guards/roles.guard.ts
+@apps/api/src/auth/dto/admin-reset-password.dto.ts
+@apps/api/src/tenant/tenant.guard.ts
+@apps/api/src/user/user.service.ts
+@apps/api/src/user/user.controller.ts
+@apps/api/src/user/user.module.ts
+@apps/api/src/user/user.service.spec.ts
+@apps/api/src/dashboard/dashboard.service.spec.ts
+@apps/api/src/tenant/tenant.controller.spec.ts
+@apps/web/src/lib/auth-actions.ts
+@apps/web/src/components/layout/header.tsx
+@apps/web/src/components/settings/account-settings-form.tsx
+@.planning/quick/260911-e2s-mandantentrennung-etappe-2-bereich-tenan/260911-e2s-PLAN.md
+@.planning/quick/260909-eor-anmeldeweg-mandantenfaehig-machen-und-al/260909-eor-SUMMARY.md
+
+
+
+
+Alle Zahlen unten sind zur Planungszeit am 2026-09-11 gegen HEAD `6236b30`
+GEMESSEN, mit der jeweils angegebenen Anweisung. Sie leiten die Untersuchung,
+sie sind KEINE Bearbeitungsvollmacht — jede Datei wird vor jeder Aenderung
+erneut gelesen, jede Zahl zur Ausfuehrungszeit erneut gemessen, und weicht
+eine Messung ab, gilt die Messung und nicht dieser Plan. Baseline zur
+Planungszeit selbst nachgemessen: 911 Tests in 59 Dateien gruen, Typpruefung
+sauber, Werkzeug 110/110 gegen `tessera-ctl-db-1` (Adresse `172.19.0.2`).
+
+**Befund A — die Zahl haelt: acht ungebundene Rohtreffer, fuenf gebundene,
+und die fuenf umzustellenden sind das Benutzermodell in drei Methoden.**
+`grep -rno "this\.prisma\.[a-zA-Z]*" apps/api/src/auth | grep -v spec`: acht
+Treffer, alle in `auth.service.ts`. DREI davon sind `this.prisma.` OHNE
+Modellnamen — die `$queryRaw`-Aufrufe der drei Anmeldefunktionen (Zeilen 80,
+188, 225): `$` liegt nicht in `[a-zA-Z]`, deshalb registriert die
+Bestandsaufnahme sie nicht als Modellzugriff, waehrend die Uebersichtstabelle
+sie als Rohtreffer zaehlt (ihr Muster laesst null Buchstaben zu). Die
+uebrigen FUENF sind das Benutzermodell: `getMe` (`findUnique` Zeile 273),
+`changePassword` (`findUnique` 312, `update` 326), `adminResetPassword`
+(`findUnique` 359, `update` 368). Gebunden
+(`grep -rno "tenantPrisma\.[a-zA-Z]*\." apps/api/src/auth | grep -v spec`):
+fuenf — `tenantPrisma.user.` in `validateUser` (115, 128) und `resetPassword`
+(250), `tenantPrisma.passwordResetToken.` in `requestPasswordReset` (208)
+und `resetPassword` (259). Das Paar `passwordResetToken` ist damit
+VOLLSTAENDIG gebunden (Stand `gebunden` im Dokument stimmt) und bleibt
+unangetastet. Nach diesem Plan: ungebunden 3 (nur die drei `$queryRaw`),
+gebunden 10; Summenzeile 83 -> 78 und 162 -> 167; Bestandsaufnahme-Zeile
+`auth.service.ts`/`user` von `gemischt` auf `gebunden`; 64 Paare und
+Klassen-Verteilung 32/17/13/2 UNVERAENDERT. Alle Zahlen zur Ausfuehrungszeit
+aus den Messanweisungen des Dokuments ableiten, nicht von hier abschreiben.
+
+**Befund B — die Grenze zwischen Anmeldeweg und Nach-Anmeldung, Glied fuer
+Glied gelesen.** ANMELDEWEG (Mandant VOR der Suche unbekannt): `validateUser`
+(aufgerufen von `local.strategy.ts` `validate()`, Route `POST /auth/login`,
+`@Public()`), `requestPasswordReset` (`POST /auth/request-reset`, `@Public()`),
+`resetPassword` (`POST /auth/reset-password`, `@Public()`). Alle drei suchen
+ueber `this.prisma.$queryRaw` mit `SELECT * FROM auth_lookup_*` — die drei
+SECURITY-DEFINER-Funktionen aus `20260909160000_auth_lookup_functions`
+(SECURITY DEFINER, STABLE, `SET search_path = public, pg_temp`, `LIMIT 1`,
+fester Spaltensatz, EXECUTE nur fuer `tessera_app`) — und binden DANACH mit
+`forTenant(this.prisma, )` fuer jeden Schreibzugriff.
+NACH-ANMELDUNG (Mandant bekannt): `getMe` (`GET /auth/me`), `changePassword`
+(`POST /auth/change-password`), `adminResetPassword`
+(`POST /auth/admin-reset-password/:userId`, `@Roles(ADMIN, SUPER_ADMIN)`) —
+keine davon `@Public()`, alle hinter dem globalen `JwtAuthGuard`; `req.user`
+kommt aus `jwt.strategy.ts` `validate()` als
+`{ id: payload.sub, username, role, tenantId }`, und `login()` signiert
+`tenantId: user.tenantId` aus der Funktionszeile. `logout` und `login`
+(Cookie) beruehren keine Datenbank. Die drei zu bindenden Methoden liegen
+also VOLLSTAENDIG auf der Nach-Anmeldungs-Seite; ihr Mandant ist ein
+signiertes Claim, das vor jedem Datenbankzugriff dieser Methoden vorliegt.
+`auth.service.ts:64-73` (Kopfkommentar von `validateUser`) und
+`docs/mandantentrennung-datenbankrolle.md:98-105` beschreiben die Anordnung
+korrekt und brauchen keine Aenderung. Der Etappe-1-SUMMARY
+(`260909-eor-SUMMARY.md:51,143`) bestaetigt, dass die drei Methoden
+ABSICHTLICH fuer Etappe 2 liegen blieben.
+
+**Befund C — die Mandantenquelle der drei Methoden ist das Claim, NICHT
+`req.tenantId`.** `tenant.guard.ts:42-56`: `req.tenantId` ist `user.tenantId`,
+fuer SUPER_ADMIN aber durch die Kopfzeile `x-tenant-id` ERSETZBAR (D-10; vier
+Sender in `apps/web/src/app/(portal)/marketplace/page.tsx` und
+`marketplace/[slug]/page.tsx`, gemessen mit `grep -rn "x-tenant-id" apps/web/src`).
+`getMe` und `changePassword` suchen die EIGENE Zeile des Anfragenden — die
+liegt in seinem eigenen Mandanten, nie im umgeschalteten. Ein SUPER_ADMIN, der
+gerade mit `x-tenant-id: B` den Marktplatz von B ansieht, wuerde sich unter
+`req.tenantId` selbst nicht finden: `getMe` null, Portalhuelle ohne Benutzer.
+Deshalb: Selbstbedienung bindet an `@CurrentUser().tenantId` (das Claim),
+wie `user.controller.ts` es fuer seine fuenf Selbstbedienungswege tut
+(`currentUser.tenantId`). `auth.controller.ts` liest heute weder `req.tenantId`
+noch `x-tenant-id` (`grep -n "req.tenantId\|x-tenant-id" apps/api/src/auth/auth.controller.ts`:
+null Treffer) — das bleibt so und wird gegatet. Ob `/auth/me` die Kopfzeile
+ueberhaupt bekommt (`auth-actions.ts:252-258` sendet nur `Cookie`), ist
+dabei unerheblich: die Entscheidung haengt an der Bauform, nicht am
+heutigen Aufrufer.
+
+**Befund D — `adminResetPassword` prueft heute KEINEN Mandanten, hat KEINEN
+Frontend-Aufrufer, und der Rollen-Praezedenzfall steht in `user.controller.ts`.**
+Controller (`auth.controller.ts:117-131`): `@Param('userId')`, Rumpf
+`AdminResetPasswordDto` (`newPassword`, `mustChangePassword` — KEIN
+Mandantenfeld, gemessen), kein `@CurrentUser()`. Dienst
+(`auth.service.ts:354-377`): `findUnique` ueber die Kennung, ungebunden — jeder
+Benutzer jedes Mandanten. Heute (ein Mandant, BYPASSRLS) harmlos; nach dem
+zweiten Mandanten setzt ein ADMIN von A das Kennwort eines Benutzers von B
+und meldet sich als dieser an (T-FH9-01). `grep -rn "admin-reset-password\|adminResetPassword" apps/web/src apps/api/src | grep -v "auth.service\|auth.controller"`:
+nur die DTO-Datei — der Weg hat KEINEN Aufrufer im Frontend und keinen im
+Backend; die Benutzerverwaltung setzt Kennwoerter ueber `PATCH /users/:id`
+mit `password` im `UpdateUserDto` (`UserService.update` hasht). Der Endpunkt
+ist damit ein UI-loser ZWEITER Weg zur selben Wirkung, ohne die
+Mandantenpruefung, die der Schwesterweg hat. Rollenverzweigung, wortgleicher
+Praezedenzfall `user.controller.ts` `resolveTargetUser` (260910-das): ADMIN ->
+`findById(currentUser.tenantId, id)` (gebunden an den EIGENEN Mandanten);
+SUPER_ADMIN -> `findByIdForPlatformAdmin(id)` (Fan-out je Mandant, gebunden
+im Rumpf; die uebergreifende Sicht der obersten Rolle ist GEWOLLT und "darf
+NICHT an den Mandanten des Aufrufers gebunden werden — das waere eine stille
+Funktionsminderung", `user.service.ts:207-230`). ENTSCHEIDUNG: exakt
+spiegeln. Controller: ADMIN -> `tenantId = currentUser.tenantId`; SUPER_ADMIN
+-> `userService.findByIdForPlatformAdmin(userId)`, nicht gefunden -> die
+heutige `BadRequestException('User not found')`, sonst
+`tenantId = ziel.tenantId`. Dienst:
+`adminResetPassword(tenantId, callerRole, userId, newPassword, mustChangePassword)`
+mit EINEM Klienten `tenantPrisma`. Dafuer importiert `AuthModule` das
+`UserModule` (exportiert `UserService`): `UserModule` importiert
+`GroupsModule`, `GroupsModule` importiert NICHTS (`grep -n "imports:" apps/api/src/groups/groups.module.ts`:
+null Treffer), kein Modul ausser `app.module.ts` importiert `AuthModule`
+(`grep -rn "AuthModule" apps/api/src --include=*.module.ts`) — zyklusfrei.
+`LdapModule`, das `AuthModule` bereits importiert, importiert `UserModule`
+selbst. Erwogen und verworfen: SUPER_ADMIN ueber `req.tenantId`/`x-tenant-id`
+— haette ohne Kopfzeile die Reichweite der obersten Rolle still auf den
+eigenen Mandanten verkuerzt und waere vom Schwesterweg `PATCH /users/:id`
+abgewichen.
+
+**Befund E — die Rechteausweitung INNERHALB des Mandanten, die dieser Bereich
+misst und nur zur Haelfte behebt.** `adminResetPassword` vergleicht heute
+KEINE Rollen: ein ADMIN setzt das Kennwort eines SUPER_ADMIN, der im selben
+Mandanten liegt, und meldet sich als Plattform-Administrator an. Derselbe
+Fall im Schwesterweg: `user.controller.ts` `update` (um Zeile 170-215)
+verhindert mit T-02-08 nur das ZUWEISEN der Rolle SUPER_ADMIN
+(`dto.role === Role.SUPER_ADMIN`), nicht das Aendern eines Benutzers, der
+diese Rolle bereits HAT — `password` im DTO geht durch; `deactivate`/`delete`
+(um 240-250) ebenso. Kein Mandantenproblem, gefunden, weil dieser Durchlauf
+jede Zeile aufschlaegt (dieselbe Art Fund wie der wirkungslose
+Selbstloesch-Riegel in 260910-das). ENTSCHEIDUNG: in DIESEM Handler
+schliessen (der Dienst kennt nach dem gebundenen `findUnique` die Rolle des
+Ziels; ist sie SUPER_ADMIN und der Aufrufer nicht, `ForbiddenException`);
+der Schwesterweg liegt ausserhalb der Erlaubnisliste dieses Plans und wird
+als OFFENER Ledger-Eintrag mit konkreter Reparatur uebergeben (T-FH9-05).
+Zur Ausfuehrungszeit an den drei Handlern erneut nachzulesen; faellt die
+Lesung anders aus, gilt die Lesung.
+
+**Befund F — die Testlage: eine Identitaets-Attrappe, Testnamen, die
+Bindung BEHAUPTEN, und drei Methoden ohne einen einzigen Test.**
+`auth.service.spec.ts` (302 Zeilen): `vi.mock` auf das Bindungshilfsmittel als
+`(p) => p` (Zeile 7-9, Kommentar verweist auf ein Muster in
+`ldap.service.spec.ts`, das dort ebenfalls noch steht). Vier
+`describe`-Bloecke, 13 Faelle: `validateUser` LDAP (8), `validateUser` lokal
+(1), `requestPasswordReset` (2), `resetPassword` (2). Die drei Faelle mit
+"mandantengebunden" im Namen pruefen `prisma.user.update`/
+`prisma.passwordResetToken.create` auf dem UNGEBUNDENEN Nachbau — sie
+bestuenden auch, wenn kein einziger Aufruf gebunden waere. KEIN Fall fuer
+`getMe`, `changePassword`, `adminResetPassword`. KEINE `auth.controller.spec.ts`
+(`ls apps/api/src/auth/`). Vorlagen: `user.service.spec.ts` (`makeFakePrisma`
+mit `rawUser`/`makeScopedUser`, `boundCallLog`, P2025/P2002-Nachbau — dieselbe
+Tabelle), `tenant.controller.spec.ts` (Controller-Attrappen, Rollen-Metadaten
+ueber `Reflect.getMetadata(ROLES_KEY, ...)`, `reflect-metadata`),
+`dashboard.service.spec.ts` ab Zeile 545 (Wachhund
+`vi.mocked(forTenant).mock.calls.length`).
+
+**Befund G — die Wegwerf-Tabelle `User` des Werkzeugs hat 10 der 15 skalaren
+Spalten; der generierte Client braucht alle.** `runAuthLookupChecks`
+(`rls-scratch-check.mjs:268-281`) legt `"User"` mit `id, username, email,
+tenantId, passwordHash, ldapDn, isActive, role, displayName,
+mustChangePassword` an; `model User` in `schema.prisma` hat dazu
+`createdAt`, `updatedAt`, `lastLoginAt`, `avatarPath`, `accentColor` (und vier
+RELATIONSFELDER `tenant`, `passwordResetTokens`, `groupMemberships`,
+`moduleGrants`, die keine Spalten sind). `findUnique` ohne `select` (Form von
+`changePassword`/`adminResetPassword`) und das `select` von `getMe` (nennt
+`avatarPath`, `accentColor`) scheitern auf dem generierten Client mit P2022,
+solange die fuenf fehlen — die dashboard-Lehre (260910-krx, Pruefung 5b).
+Typen aus den ausgelieferten Migrationen: `createdAt TIMESTAMP(3) NOT NULL
+DEFAULT CURRENT_TIMESTAMP`, `updatedAt TIMESTAMP(3) NOT NULL` (ohne Vorgabe
+— fuer die vorhandenen Wegwerf-Zeilen braucht es eine, Abweichung wie bei
+`Tenant` in 260911-e2s), `lastLoginAt TIMESTAMP(3)` (alle drei aus
+`20260618112124`, CREATE TABLE "User"), `avatarPath TEXT`
+(`20260630095533_add_user_avatar`), `accentColor TEXT`
+(`20260702000000_add_user_accent_color`). `readSchemaModelFieldNames()`
+zaehlt Relationsfelder mit (Befund M aus 260911-e2s) — fuer `User` ist ein
+Helfer noetig, der Felder ueber ihren TYP ausschliesst (zweites Wort der
+Zeile, `?`/`[]` abgestreift, ist der Name eines anderen `model` im Schema ->
+keine Spalte; `Role` ist ein `enum`, kein Modell, und bleibt). Zustand der
+Wegwerf-Datenbank an der Stelle, wo der neue Abschnitt laeuft (aus der
+Ausgabe von 260911-e2s, zur Ausfuehrungszeit ueber die Wartungsrolle
+nachzumessen, nicht anzunehmen): `Tenant` hat A und B (C geloescht), der
+Fremdschluessel `User_tenantId_fkey` ist nachgeruestet, `User` hat je zwei
+Zeilen in A und B (`user-a`, `user-a2`, `user-b`, `user-b2`);
+`runTransactionShapeMeasurement` und `runConcurrencyProbe` fassen `"User"`
+nicht an (260911-e2s, Befund M) — der neue Abschnitt ist ein Blatt: NACH
+`runTenantAreaChecks`, VOR `runTransactionShapeMeasurement`.
+
+**Befund H — was "die Anmeldefunktionen unangetastet" MESSBAR heisst.** Auf
+der Codeseite: die drei `$queryRaw`-Stellen bleiben auf `this.prisma`
+(ein gebundener `$queryRaw` liefe laut Kopf von `prisma-tenant.extension.ts`
+durch `$allOperations` mit gesetztem Kontext — fuer eine SECURITY-DEFINER-
+Funktion wirkungslos, aber ein falsches Signal fuer jeden Leser, der daraus
+"die Suche ist gebunden" liest), `git diff --name-only 6236b30 -- apps/api/prisma`
+bleibt leer. Auf der Datenbankseite: das Werkzeug spielt die Migration in
+`runAuthLookupChecks` bereits ein (`tessera_app` -> Wegwerf-Rolle); der neue
+Abschnitt liest `pg_proc` (`prosecdef`, `provolatile`, `proconfig`,
+`pg_get_functiondef`) und misst, dass alle drei Funktionen SECURITY DEFINER,
+STABLE, mit `search_path=public, pg_temp` und `LIMIT 1` sind, und dass
+`auth_lookup_user_by_username('alice')` nach der Spaltenerweiterung
+weiterhin genau EINE Zeile mit genau NEUN Spalten liefert — der feste
+Spaltensatz laesst die fuenf neuen Spalten NICHT durch. Das ist die Messung
+von "nichts weitet aus, was die Funktionen preisgeben".
+
+**Befund I — Etappe 3 und was sie hier wieder aufmacht.** Die Etappe-3-
+Entscheidung (1) macht `username` (und `email`) je Mandant eindeutig. Davon
+betroffen, alles NICHT dieser Auftrag: `auth_lookup_user_by_username(p_username)`
+(stuetzt sich auf `username @unique` plattformweit; braucht kuenftig
+`(p_tenant_id, p_username)` — die Funktion wird dabei ENGER, zwei
+Gleichheitsbedingungen statt einer, nicht weiter), gegebenenfalls
+`auth_lookup_user_by_email`, `local.strategy.ts` (kennt nur
+`username`/`password` — braucht eine Mandantenangabe VOR der Suche, etwa
+Mandantenkuerzel im Anmeldeformular oder aus dem Host), die `@unique`-Indizes
+auf `User` (`@@unique([tenantId, username])`), `UserService.findByUsername`,
+`resolveEmailForWrite` (ldap), die P2002-Uebersetzung in `UserService.create`/
+`update` (WINDOWS #22). Was dieser Plan tut, ist dafuer NEUTRAL: die Bindung
+haengt am Claim `tenantId` (das es unabhaengig davon gibt, WIE der Anmeldeweg
+den Mandanten ermittelt) und an `User.id` (`@default(uuid())`, plattformweit
+eindeutig — Kette aus 260911-cwh: Schema -> `auth.service.ts` `sub: user.id`
+-> `JwtStrategy.validate` -> `@CurrentUser().id`). Was Etappe 3 SCHWERER
+gemacht haette und deshalb unterbleibt: Selbstbedienung ueber `req.tenantId`
+binden; irgendwo nach der Anmeldung den Mandanten aus `username`/`email`
+ableiten; die Funktionen um Spalten erweitern.
+
+**Befund J — kein Hintergrunddienst, keine Transaktion, keine
+Relationseinbindung.** `grep -rn "@Cron\|onModuleInit\|onApplicationBootstrap\|setInterval\|setTimeout" apps/api/src/auth --include=*.ts`
+und `grep -rn '\$transaction(' apps/api/src/auth --include=*.ts`: je null
+Treffer. `grep -rn "include:\|_count\|select:" apps/api/src/auth --include=*.ts | grep -v spec`:
+genau EIN Treffer, das `select` in `getMe` (Zeile 275) — ausschliesslich
+skalare Felder von `User`, keine Relation (Fehler 9 des Vorhabens, WINDOWS
+#27: hier keine Auspraegung). Andere `prisma`-Nennungen im Bereich
+(`roles.decorator.ts`, `roles.guard.ts`, `auth.controller.ts`) sind der
+Typ-Import `Role` aus `@prisma/client`, kein Datenbankzugriff. Kein
+sechster Fall der Hintergrunddienst-Falle.
+
+**Befund K — die umgekehrte Fehlerrichtung je Methode, gelesen (zur
+Ausfuehrungszeit an jedem Glied erneut nachzulesen).** (1) `getMe` liefert
+`null`; der Controller gibt `null` zurueck; NestJS' Express-Adapter sendet bei
+`isNil(body)` einen LEEREN Rumpf mit Status 200 (nachzulesen in
+`apps/api/node_modules/@nestjs/platform-express/adapters/express-adapter.js`,
+Methode `reply`); `fetchCurrentUser` (`auth-actions.ts:243-270`) laeuft mit
+`response.json()` auf den leeren Rumpf, faengt im `catch` und liefert `null`;
+`header.tsx:26-40` (`if (u) setUser(...)`) und
+`account-settings-form.tsx:46-53` (`if (u) ...`) tun bei `null` NICHTS. Folge:
+die Portalhuelle rendert OHNE angemeldeten Benutzer — kein Name, kein
+Avatar, `isAdmin` falsch, Admin-Navigation weg — und `fetchCurrentUser`
+liefert fuer "nicht angemeldet" und "Zeile unsichtbar" denselben Wert. Das
+ist NICHT laut (der Auftrag vermutete "laut"), sondern die Familie von
+WINDOWS #23/#25/#26. Die Seite `change-password/page.tsx:20` haengt an
+demselben Aufruf; `ForcePasswordChangeInterceptor` laesst `/auth/me` und
+`/auth/change-password` ausdruecklich durch (Zeilen 56-58) — der erzwungene
+Kennwortwechsel braucht `getMe`. (2) `changePassword`: gebundener `findUnique`
+liefert `null` -> `UnauthorizedException('User not found or has no local password')`,
+401 -> `auth-actions.ts:128-133` uebersetzt NUR `Current password is incorrect`
+in `wrongCurrentPassword`, alles andere in `networkError` — der Nutzer sieht
+eine Netzwerkfehler-Meldung fuer einen Trennungsfehler (der Auftrag vermutete
+"falsches altes Kennwort"; gemessen ist es `networkError`). (3)
+`adminResetPassword`: `BadRequestException('User not found')`, 400, kein
+UI-Aufrufer — fuer einen API-Aufrufer "diesen Benutzer gibt es nicht", und
+fuer ein fremdmandantiges Ziel ist das nach der Bindung die RICHTIGE Antwort
+(keine Existenzaussage ueber einen fremden Mandanten, T-DAS-08-Form). (4)
+Anmeldeweg: unveraendert — `validateUser` null -> 401 `Invalid credentials`,
+vom Scharfschalten nicht betroffen (Funktionen). Etappe-4-Vorabpruefung: fuer
+einen bekannten Benutzer die Zeile ueber die Wartungsrolle lesen und den
+gebundenen `findUnique` unter seinem Claim-Mandanten daneben halten.
+
+**Befund L — die Buchfuehrung nach diesem Plan.** Uebersichtszeile `auth`:
+heute 8/5; danach 3/10 (die drei `$queryRaw`-Rohtreffer bleiben und sind
+KEINE Modellzugriffe — das gehoert in den Hinweis). Summenzeile: 83 -> 78,
+162 -> 167. Bestandsaufnahme: `auth.service.ts`/`user` von `gemischt` auf
+`gebunden`, Begruendung neu; `auth.service.ts`/`passwordResetToken` bleibt
+`gebunden`, unveraendert. Klassen-Verteilung: 64 Paare, 32/17/13/2, KEINE
+Verschiebung — als `**Stand 260911-fh9**`-Absatz ausdruecklich festgehalten
+(die Form von 260910-krx/260911-cwh). Hintergrunddienst-Abschnitt: ein
+PLAIN-Absatz, kein sechster Fall (Befund J). `Was diese Etappe NICHT
+entscheidet`: ein NEUER Punkt zum Etappe-3-Umbau des Anmeldewegs (Befund I).
+Keine Aenderung an `docs/anleitung-entwicklung.md` (nennt keine der drei
+Methoden, gemessen) und keine an `docs/mandantentrennung-datenbankrolle.md`
+(Abschnitt 3 beschreibt die Anordnung korrekt).
+
+
+
+
+
+
+ Aufgabe 1: Die Fehlerrichtung fuer diesen Bereich MESSEN und aufschreiben — die Grenze zwischen Anmeldeweg (Funktionen) und Nach-Anmeldung (gebunden), ueber den generierten Client
+ Der lokale Datenbank-Container `tessera-ctl-db-1` laeuft; `docker inspect tessera-ctl-db-1` liefert eine Adresse. Ohne ihn kann das Wegwerf-Werkzeug nichts messen und die Aufgabe ist zu stoppen, nicht zu schaetzen.
+ apps/api/scripts/rls-scratch-check.mjs, docs/mandantentrennung-etappe2-fehlerrichtung.md
+ docs/mandantentrennung-datenbankrolle.md (Abschnitt 3, den Absatz zum Anmeldeweg VOLLSTAENDIG — bevor irgendetwas angefasst wird), apps/api/prisma/migrations/20260909160000_auth_lookup_functions/migration.sql (VOLLSTAENDIG), apps/api/scripts/rls-scratch-check.mjs (Kopf, `report`, `forTenantQuery`, `withAdminPrisma`, `runAuthLookupChecks` VOLLSTAENDIG (Anlage von "User", Einspielen der Migration), `sqlStateOf`, `runUserAreaChecks` (Kopfkommentar zur Reihenfolgebedingung), `readSchemaModelFieldNames`, `runCalendarAreaChecks` (Pruefung 8 und die Client-Pruefungen 9-12), `runTenantAreaChecks` VOLLSTAENDIG (Spaltenerweiterung von "Tenant", Fremdschluessel, welche Zeilen am Ende stehen), `buildInlineExtendedClient`, `main`), apps/api/prisma/schema.prisma (model User, model PasswordResetToken, enum Role), apps/api/prisma/migrations/20260618112124_auth_multi_tenancy/migration.sql (CREATE TABLE "User"), apps/api/prisma/migrations/20260630095533_add_user_avatar/migration.sql, apps/api/prisma/migrations/20260702000000_add_user_accent_color/migration.sql, apps/api/src/auth/auth.service.ts (VOLLSTAENDIG), apps/api/src/auth/auth.controller.ts (VOLLSTAENDIG), apps/api/src/auth/strategies/jwt.strategy.ts, apps/api/src/auth/strategies/local.strategy.ts, apps/api/src/auth/interceptors/force-password-change.interceptor.ts, apps/api/src/tenant/tenant.guard.ts, apps/api/src/user/user.controller.ts (`resolveTargetUser`, `update`, `deactivate`, `delete`), apps/api/src/user/user.service.ts (`findByIdForPlatformAdmin` samt Kopfkommentar), apps/web/src/lib/auth-actions.ts (`changePasswordAction`, `fetchCurrentUser`), apps/web/src/components/layout/header.tsx (den `useEffect` um Zeile 26), apps/web/src/components/settings/account-settings-form.tsx (den `useEffect` um Zeile 46), apps/web/src/app/(portal)/change-password/page.tsx, apps/api/node_modules/@nestjs/platform-express/adapters/express-adapter.js (Methode `reply`), docs/mandantentrennung-etappe2-fehlerrichtung.md (Abschnitte `## Bereich user` (u1)-(u5) und `## Bereich tenant` (n1)-(n5) vollstaendig, als Form)
+
+TEIL 1 — die Messung. Erweitere `apps/api/scripts/rls-scratch-check.mjs` um
+einen dreizehnten Abschnitt `runAuthAreaChecks(adminUrl, scratchRoleUrl, results)`
+— ausdruecklich GETRENNT vom bestehenden `runAuthLookupChecks`, der die drei
+Anmeldefunktionen misst — und rufe ihn in `main()` NACH `runTenantAreaChecks`
+und VOR `runTransactionShapeMeasurement` auf. Der Abschnitt setzt auf der
+vorhandenen Wegwerf-Tabelle `"User"` auf (aus `runAuthLookupChecks`, mit
+Zeilenschutz, wortgleicher Regel, seit `runTenantAreaChecks` mit
+Fremdschluessel auf `"Tenant"`) und auf der eingespielten Funktions-Migration;
+keine spaetere Pruefung setzt auf seinen Aenderungen auf (Befund G). Halte
+die Reihenfolgebedingung im Kopfkommentar fest, wie `runUserAreaChecks` es
+vormacht, und nenne dort in EINEM Satz, warum dieser Abschnitt neben
+`runAuthLookupChecks` steht: jener misst den Anmeldeweg VOR bekanntem
+Mandanten (Funktionen), dieser die drei Methoden NACH der Anmeldung
+(gebundener Modellzugriff) — die Grenze, die dieser Plan festnagelt.
+
+Vorbereitung ueber die Wartungsrolle: (a) `ALTER TABLE "User"` um die fuenf
+fehlenden Spalten `createdAt`, `updatedAt`, `lastLoginAt`, `avatarPath`,
+`accentColor` mit den Typen aus den drei ausgelieferten Migrationen (Befund
+G); `updatedAt` bekommt fuer die vorhandenen Wegwerf-Zeilen eine Vorgabe
+`CURRENT_TIMESTAMP`, die die Migration nicht hat — vermerke das im Kommentar
+als Abweichung, die nur das Nachruesten betrifft (Prisma setzt den Wert
+clientseitig, `@updatedAt`). (b) Lege einen neuen Helfer
+`readSchemaModelScalarFieldNames(modelName)` neben `readSchemaModelFieldNames`
+an: er liest wie jener die Feldzeilen des Modells, ermittelt aber zusaetzlich
+die Menge aller `model `-Namen des Schemas und laesst jedes Feld weg,
+dessen Typ (zweites Wort der Zeile, `?` und `[]` abgestreift) ein solcher
+Modellname ist — Relationsfelder haben keine Spalte (Befund M aus 260911-e2s,
+dort fuer `Tenant` ueber die Migration umgangen; hier ist die Spaltenmenge
+ueber drei Migrationen verteilt, deshalb der Weg ueber das Schema mit
+Relationsfilter). `Role` ist ein `enum` und bleibt. (c) Miss ueber die
+Wartungsrolle, welche `User`-Zeilen und Mandanten tatsaechlich vorhanden sind
+(Befund G nennt den erwarteten Stand — nicht annehmen), und benutze fuer die
+Pruefungen unten einen Benutzer aus TENANT-A (`user-a`, Kennwort-Hash
+`hash-a`) und den Mandanten TENANT-B als "fremden Administrator". Alle
+Pruefungen laufen unter der Rolle ohne `BYPASSRLS`; Vergleichswerte kommen
+ueber die Wartungsrolle.
+
+Mindestens zehn namentlich benannte Pruefungen, jede mit einer Belegausgabe,
+die die beobachteten Werte nennt (Konstruktorname und `code` woertlich, wo ein
+Fehler erwartet wird — rate das Ergebnis nicht vorweg):
+
+1. `auth-anmeldefunktionen-security-definer-unveraendert` — ueber die
+ Wartungsrolle `pg_proc` fuer `proname LIKE 'auth_lookup_%'`: genau DREI
+ Zeilen (`auth_lookup_user_by_username`, `auth_lookup_user_by_email`,
+ `auth_lookup_reset_token`), jede mit `prosecdef = true`,
+ `provolatile = 's'`, `proconfig` enthaelt `search_path=public, pg_temp`,
+ und `pg_get_functiondef(oid)` enthaelt `LIMIT 1`. Die Belegausgabe nennt
+ die drei Namen und die vier Eigenschaften je Funktion. Das ist die
+ Datenbankseite von "nichts an der Anordnung angefasst".
+2. `auth-wegwerftabelle-user-deckt-alle-spalten-des-generierten-clients` —
+ Spaltenmenge der Wegwerf-Tabelle aus `information_schema.columns`
+ identisch mit `readSchemaModelScalarFieldNames('User')` (Befund G:
+ fuenfzehn). Steht VOR den Client-Pruefungen; faellt sie durch, bricht der
+ Abschnitt ab (Form von Pruefung 8 in `runCalendarAreaChecks`).
+3. `auth-anmeldesuche-findet-benutzer-weiterhin-mit-festem-spaltensatz` —
+ unter der Wegwerf-Rolle `SELECT * FROM auth_lookup_user_by_username('alice')`:
+ genau EINE Zeile mit genau NEUN Schluesseln, und weder `avatarPath` noch
+ `accentColor` noch `email` darunter — der feste Spaltensatz der Funktion
+ laesst die Spaltenerweiterung NICHT durch. Belegausgabe nennt die
+ Schluessel.
+4. `auth-getme-generierter-client-ungebunden-liefert-null` — die tragende
+ Belegzeile: `prisma.user.findUnique` mit der Kennung von `user-a` und dem
+ `select`, das `getMe` heute stellt (die zehn Felder aus `auth.service.ts`,
+ Zeile 275-286, zur Laufzeit dort abzulesen), UNGEBUNDEN auf dem
+ generierten Client: `null`, waehrend die Wartungsrolle die Zeile sieht.
+ Die Belegausgabe sagt beim Namen: das ist der Wert, den `GET /auth/me`
+ nach dem Scharfschalten als leeren Rumpf ausliefert.
+5. `auth-getme-generierter-client-gebunden-eigener-mandant-findet-benutzer` —
+ dieselbe Abfrage ueber `buildInlineExtendedClient(prisma, 'TENANT-A')`:
+ Zeile gefunden, `tenantId === 'TENANT-A'`, genau die zehn selektierten
+ Schluessel.
+6. `auth-getme-generierter-client-gebunden-fremder-mandant-liefert-null` —
+ dieselbe Abfrage gebunden unter TENANT-B: `null`. Belegausgabe: ein
+ Administrator von TENANT-B sieht `user-a` nicht — die Datenbankseite von
+ T-FH9-01.
+7. `auth-changepassword-generierter-client-ungebundenes-update-scheitert-laut`
+ — die Schreibform, die `changePassword` heute stellt:
+ `prisma.user.update({ where: { id: 'user-a' }, data: { passwordHash: 'hash-a-neu-ungebunden', mustChangePassword: false } })`
+ UNGEBUNDEN: bestanden genau dann, wenn ein Fehler geworfen wird UND die
+ Wartungsrolle danach noch `hash-a` liest. Konstruktorname und `code`
+ woertlich.
+8. `auth-changepassword-generierter-client-gebundenes-update-eigener-mandant-gelingt`
+ — dasselbe `update` mit `passwordHash: 'hash-a-neu'` gebunden unter
+ TENANT-A: gelingt, die Wartungsrolle liest `hash-a-neu`, `updatedAt` ist
+ nicht null (bestaetigt nebenbei, dass die Wegwerf-Tabelle die clientseitig
+ gesetzten Werte annimmt).
+9. `auth-adminreset-generierter-client-gebundenes-update-fremder-mandant-scheitert-laut`
+ — `update` mit `passwordHash: 'hash-a-fremd'` auf `user-a`, gebunden unter
+ TENANT-B: bestanden genau dann, wenn ein Fehler geworfen wird UND die
+ Wartungsrolle danach weiterhin `hash-a-neu` liest. Belegausgabe: ein
+ ADMIN von TENANT-B kann das Kennwort von `user-a` nicht setzen — gemessen,
+ nicht behauptet.
+10. `auth-fan-out-je-mandant-gebunden-loest-mandant-der-kennung-auf` — die
+ Form, die Aufgabe 2/3 fuer die oberste Rolle benutzt:
+ `prisma.tenant.findMany` ungebunden als Treiber (Tenant ohne Regel,
+ gemessen in 260911-e2s), dann je Mandant
+ `buildInlineExtendedClient(prisma, t.id).user.findUnique({ where: { id: 'user-a' } })`:
+ genau EIN Treffer, unter TENANT-A, mit `tenantId === 'TENANT-A'`. Das
+ belegt, dass die Kennung allein den Mandanten des Ziels ergibt — weil
+ `User.id` plattformweit eindeutig ist.
+
+Ergaenzt wird nur, gestrichen wird nicht; nenne im SUMMARY die tatsaechlich
+gezaehlte Zahl, nicht diese.
+
+TEIL 2 — die Codeaussagen, jede mit ihrer Reichweite. Fuehre die
+Nachpruefungen aus den Befunden B, C, D, E, J und K tatsaechlich aus und
+notiere jeweils die Anweisung oder Datei-und-Zeile, damit jede Aussage
+widerlegbar bleibt: (B) die Grenze Glied fuer Glied — je Methode: Route,
+`@Public()` ja/nein, Suche ueber Funktion oder Modell, Bindung nach dem Fund;
+dazu `jwt.strategy.ts` `validate()` und `login()` fuer die Herkunft des Claims;
+(C) `req.tenantId` und `x-tenant-id` in `auth.controller.ts` (null Treffer),
+die Kopfzeilen-Sender im Frontend, die Selbstbedienungsform in
+`user.controller.ts`; (D) alle Aufrufer von `adminResetPassword`/
+`admin-reset-password` in `apps/api/src` und `apps/web/src`, das DTO ohne
+Mandantenfeld, `resolveTargetUser` als Praezedenzfall, die Modulgraphen-
+Messung (`grep -n "imports:" apps/api/src/groups/groups.module.ts`,
+`grep -rn "AuthModule" apps/api/src --include=*.module.ts`); (E) die drei
+Handler `update`, `deactivate`, `delete` in `user.controller.ts` auf eine
+Rollenpruefung des ZIELS — Zeilen nennen; (J) die drei Anweisungen aus
+Befund J; (K) die vier Glieder der `getMe`-Kette (Adapter `reply`,
+`fetchCurrentUser`, `header.tsx`, `account-settings-form.tsx`) und die
+Uebersetzungstabelle in `changePasswordAction`. Faellt eine Nachpruefung
+ANDERS aus als in den Planungsbefunden, gilt die Messung; schreibe sie auf
+und benenne die Abweichung ausdruecklich. Findet (E) KEINE Luecke, entfaellt
+der Ledger-Eintrag T-FH9-05 in Aufgabe 3, und das steht dann im SUMMARY mit
+den gelesenen Zeilen.
+
+TEIL 3 — die Kritikschrift. Erweitere
+`docs/mandantentrennung-etappe2-fehlerrichtung.md` um einen Abschnitt
+`## Bereich auth` unmittelbar VOR `## Verweis`, in der Form der vorhandenen
+Bereichsabschnitte, mit fuenf Unterabschnitten unter dem Buchstaben `h` (von
+aut-h; `a` ist durch die Abschnitte (a)-(e) des ldap-Kopfes belegt):
+
+- `### (h1) Die Messung` — die tatsaechlich beobachtete Werkzeugausgabe
+ woertlich eingerueckt, die tragende Belegzeile (Pruefung 4) benannt, die
+ Trennung von `runAuthLookupChecks` erklaert, die `pg_proc`-Messung
+ (Pruefung 1) und der feste Spaltensatz (Pruefung 3) als Beleg dafuer, dass
+ die Anmeldefunktionen unangetastet sind und nichts Zusaetzliches
+ preisgeben. Nenne, welche Pruefungen ueber den generierten Client laufen
+ und warum (`findUnique` ohne `select` und das `select` von `getMe` sind
+ Client-Formen; Fehler 7 des Vorhabens), und die Spaltenerweiterung mit
+ Relationsfilter (Befund G).
+- `### (h2) Signaltabelle je Pfad` — je Methode des Dienstes eine Zeile,
+ BEIDE Seiten der Grenze: `validateUser`, `requestPasswordReset`,
+ `resetPassword` (Anmeldeweg: Funktion, dann gebunden — vom Scharfschalten
+ NICHT betroffen, mit der Pruefung aus `runAuthLookupChecks`, die das
+ belegt), `login`/`logout` (kein Datenbankzugriff), `getMe`,
+ `changePassword`, `adminResetPassword` (ADMIN-Zweig und SUPER_ADMIN-Zweig
+ getrennt): Verhalten HEUTE nach dem Scharfschalten ohne diesen Plan, das
+ konkrete Signal mit Statuscode und woertlicher Meldung, und in eigener
+ Spalte, ob das Frontend es durchlaesst (mit Datei und Zeile).
+- `### (h3) Welcher Code Leere als Abwesenheit deutet` — die `getMe`-Kette
+ aus Befund K mit allen vier Gliedern namentlich (Adapter, `auth-actions.ts`,
+ `header.tsx`, `account-settings-form.tsx`, dazu `change-password/page.tsx`
+ und der Interceptor), mit dem ausdruecklichen Satz, dass der Auftrag hier
+ "laut" vermutete und die Lesung "verschluckt" ergibt — `fetchCurrentUser`
+ liefert fuer "nicht angemeldet" und "Zeile unsichtbar" denselben Wert;
+ `changePassword` mit der Uebersetzungstabelle (`networkError`, nicht
+ "falsches Kennwort" — der Auftrag vermutete das Zweite); `adminResetPassword`
+ ohne UI-Aufrufer und mit der Bemerkung, dass `User not found` fuer ein
+ fremdmandantiges Ziel nach der Bindung die RICHTIGE Antwort ist.
+- `### (h4) Was dieser Durchlauf bewusst nicht löst` — (a) ETAPPE 3 in einem
+ eigenen Absatz: was die Entscheidung (1) am Anmeldeweg wieder aufmacht
+ (Befund I, jede Stelle namentlich), dass die Funktionen dabei ENGER werden
+ (zwei Gleichheitsbedingungen), warum dieser Plan neutral ist (Claim und
+ `User.id`, Kette aus 260911-cwh), und was er deshalb NICHT tut
+ (`req.tenantId`, Ableitung aus `username`/`email`, Spaltenerweiterung der
+ Funktionen); (b) die Rechteausweitung ADMIN -> SUPER_ADMIN im Schwesterweg
+ `PATCH /users/:id` (und `deactivate`/`delete`) mit den gelesenen Zeilen,
+ der Reparatur in einem Satz (Rolle des ZIELS pruefen, nicht nur die
+ zugewiesene) und der Entscheidung fuer einen Ledger-Eintrag (ausserhalb
+ der Erlaubnisliste; wird in Aufgabe 3 angelegt); (c) das Frontend, das
+ `null` verschluckt (nicht angefasst; Ledger-Eintrag in Aufgabe 3, Familie
+ #23/#25/#26); (d) die fehlende Existenzpruefung des `x-tenant-id`-Werts
+ (bekannt aus (n4)(f), hier nur, weil sie die Entscheidung gegen
+ `req.tenantId` stuetzt); (e) die Etappe-4-Vorabpruefung aus Befund K;
+ (f) dass `changePassword` zwei verschiedene 401-Meldungen hat
+ (kein lokales Kennwort vs. falsches Kennwort) — bestehend, nicht Teil
+ dieses Auftrags.
+- `### (h5) Was dieser Durchlauf bewusst nicht anfasst` — die drei
+ Funktionen und ihre Migration; die drei `$queryRaw`-Stellen; `local.strategy.ts`
+ und `jwt.strategy.ts`; das DTO; `docs/mandantentrennung-datenbankrolle.md`
+ Abschnitt 3 (beschreibt die Anordnung korrekt); `docs/anleitung-entwicklung.md`
+ (nennt keine der drei Methoden); `user.controller.ts` (nur gelesen);
+ das Frontend (nur beschrieben); Schema und Migrationen; die Kopfzeile in
+ `auth.service.spec.ts` Zeile 4-6, die auf ein Muster in
+ `ldap.service.spec.ts` verweist — wird in Aufgabe 2 ersetzt, hier nur
+ als Befund F genannt.
+
+Aendere in dieser Aufgabe KEINE Datei unter `apps/api/src`, KEINE unter
+`apps/api/prisma` und KEINE unter `apps/web`.
+
+
+ DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && OUT=$(TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs) && echo "$OUT" && for K in auth-anmeldefunktionen-security-definer-unveraendert auth-wegwerftabelle-user-deckt-alle-spalten-des-generierten-clients auth-anmeldesuche-findet-benutzer-weiterhin-mit-festem-spaltensatz auth-getme-generierter-client-ungebunden-liefert-null auth-getme-generierter-client-gebunden-eigener-mandant-findet-benutzer auth-getme-generierter-client-gebunden-fremder-mandant-liefert-null auth-changepassword-generierter-client-ungebundenes-update-scheitert-laut auth-changepassword-generierter-client-gebundenes-update-eigener-mandant-gelingt auth-adminreset-generierter-client-gebundenes-update-fremder-mandant-scheitert-laut auth-fan-out-je-mandant-gebunden-loest-mandant-der-kennung-auf; do echo "$OUT" | grep -q "^$K: bestanden" || { echo "FEHLENDE ODER FEHLGESCHLAGENE PRUEFUNG: $K"; exit 1; }; done && echo "$OUT" | grep -qE '^Alle [0-9]+ Pruefungen bestanden\.$' && N=$(echo "$OUT" | sed -nE 's/^Alle ([0-9]+) Pruefungen bestanden\.$/\1/p') && { test "$N" -ge 120 || { echo "PRUEFUNGSZAHL: $N, erwartet mindestens 120 (110 bisherige plus mindestens 10 neue)"; exit 1; }; } && grep -q 'async function runAuthAreaChecks' apps/api/scripts/rls-scratch-check.mjs && grep -q 'async function runAuthLookupChecks' apps/api/scripts/rls-scratch-check.mjs && grep -q 'function readSchemaModelScalarFieldNames' apps/api/scripts/rls-scratch-check.mjs && grep -q 'pg_proc' apps/api/scripts/rls-scratch-check.mjs && grep -q 'pg_get_functiondef' apps/api/scripts/rls-scratch-check.mjs && awk '/await runTenantAreaChecks\(/{c=NR} /await runAuthAreaChecks\(/{n=NR} /await runTransactionShapeMeasurement\(/{t=NR} END{ if(!(c&&n&&t&&c&1) && echo "$TOUT" | tail -6 && T=$(echo "$TOUT" | sed -nE 's/^ *Tests +([0-9]+) passed.*/\1/p' | head -1) && { test -n "$T" && test "$T" -ge 911 || { echo "TESTZAHL: ${T:-unbekannt}, erwartet mindestens 911"; exit 1; }; } && npm --prefix apps/api run type-check && git rev-parse --verify 6236b30 >/dev/null && PRISMA_CHANGED=$(git diff --name-only 6236b30 -- apps/api/prisma) && { test -z "$PRISMA_CHANGED" || { printf 'SCHEMA/MIGRATION GEAENDERT — in diesem Plan verboten:\n%s\n' "$PRISMA_CHANGED"; exit 1; }; } && CHANGED=$(git diff --name-only 6236b30) && UNEXPECTED=$(printf '%s\n' "$CHANGED" | grep -v '^$' | grep -vE '^(apps/api/scripts/rls-scratch-check\.mjs|docs/mandantentrennung-etappe2-fehlerrichtung\.md|\.planning/.*)$' || true) && { test -z "$UNEXPECTED" || { printf 'UNERWARTETE AENDERUNGEN AUSSERHALB DES PLANUMFANGS:\n%s\n' "$UNEXPECTED"; exit 1; }; }
+
+ `apps/api/scripts/rls-scratch-check.mjs` hat einen dreizehnten Abschnitt `runAuthAreaChecks`, getrennt von `runAuthLookupChecks`, in der richtigen Reihenfolge, mit mindestens zehn neuen, namentlich benannten Pruefungen, davon mindestens sechs ueber den generierten Client an einer Wegwerf-Tabelle `User`, deren Spaltenmenge zur Laufzeit gegen die skalaren Felder des Schemas geprueft wird (neuer Helfer mit Relationsfilter); die drei Anmeldefunktionen sind ueber `pg_proc` als SECURITY DEFINER, STABLE, mit festem Suchpfad und LIMIT 1 gemessen und geben nach der Spaltenerweiterung weiterhin genau neun Spalten preis; alle Pruefungen des Werkzeugs bestehen (mindestens 120). `docs/mandantentrennung-etappe2-fehlerrichtung.md` hat einen Abschnitt `## Bereich auth` unmittelbar vor `## Verweis` mit (h1) bis (h5), die tatsaechlich beobachtete Werkzeugausgabe woertlich, die Signaltabelle fuer BEIDE Seiten der Grenze, die `getMe`-Kette mit allen Gliedern und dem Befund "verschluckt, nicht laut", die `changePassword`-Uebersetzung als `networkError`, den Etappe-3-Absatz, den Schwesterweg mit gelesenen Zeilen. Baseline gehalten: mindestens 911 Tests gruen, Typpruefung sauber. Unter `apps/api/src`, `apps/api/prisma`, `apps/web` und den Compose-/Umgebungsdateien ist nichts geaendert.
+
+
+
+ Aufgabe 2: Die drei Methoden binden, die Signaturen und jeden Aufrufer umstellen, den Mandanten ausschliesslich aus dem Sitzungsnachweis nehmen — und die Testlage von der Identitaets-Attrappe auf zwei Klienten heben
+ apps/api/src/auth/auth.service.ts, apps/api/src/auth/auth.service.spec.ts, apps/api/src/auth/auth.controller.ts, apps/api/src/auth/auth.module.ts
+ apps/api/src/auth/auth.service.ts (VOLLSTAENDIG, erneut), apps/api/src/auth/auth.service.spec.ts (VOLLSTAENDIG), apps/api/src/auth/auth.controller.ts (VOLLSTAENDIG), apps/api/src/auth/auth.module.ts, apps/api/src/user/user.service.spec.ts (`makeFakePrisma`, `rawUser`, `makeScopedUser`, `throwNotFound`, `boundCallLog`), apps/api/src/user/user.controller.ts (`resolveTargetUser`, Kopfkommentar der Klasse), apps/api/src/user/user.service.ts (`findByIdForPlatformAdmin`), apps/api/src/user/user.module.ts, apps/api/src/dashboard/dashboard.service.spec.ts (Zeilen 545-570, Wachhund), apps/api/src/prisma/rls-access-inventory.spec.ts (`analyzeFile` — die Zuweisungsform `const X = forTenant(`), docs/mandantentrennung-etappe2-fehlerrichtung.md (Abschnitt `## Bereich auth` aus Aufgabe 1, besonders (h2) und (h4)(a))
+
+ZUERST die Testlage, DANN der Umbau. Baue `apps/api/src/auth/auth.service.spec.ts`
+so um, dass die Identitaets-Attrappe verschwindet: `vi.mock` auf das
+Bindungshilfsmittel wird auf `prisma.__makeBoundClient(tenantId)` umgeleitet
+(Form von `user.service.spec.ts`, dieselbe Tabelle). Ein handgerollter
+`makeFakePrisma()` mit In-Memory-Zeilen fuer `user` (Map ueber die Kennung,
+Felder wie die Funktionszeilen plus `avatarPath`/`accentColor`/`role`) und
+einem Bindungsprotokoll `boundCallLog` (`{ tenantId, model, method, args }`):
+
+- Der UNGEBUNDENE Nachbau hat `$queryRaw` (die bestehende Tagged-Template-
+ Attrappe `fakeQueryRaw`, die SQL-Textstuecke und Werte aufzeichnet und
+ konfigurierbare Zeilen liefert) und `__makeBoundClient` — aber KEIN
+ `user`- und KEIN `passwordResetToken`-Modell. Ein versehentlich
+ ungebundener Modellzugriff scheitert mit "Cannot read properties of
+ undefined" (die dkv-Form der Falsifizierung).
+- Der GEBUNDENE Klient (`__makeBoundClient(tenantId)`) bietet
+ `user.findUnique` (liefert die Zeile nur, wenn ihr `tenantId` dem Klienten
+ entspricht, sonst `null`; wendet ein uebergebenes `select` an),
+ `user.update` (wirft bei unsichtbarer Zeile einen Fehler mit `code: 'P2025'`
+ und der Prisma-Meldung "Record to update not found", sonst Merge und
+ Rueckgabe), `passwordResetToken.create`/`update` — und KEIN `$queryRaw`.
+ Eine gebundene Anmeldesuche scheitert damit ebenso hart wie ein
+ ungebundener Modellzugriff. Jeder Aufruf schreibt ins Protokoll.
+
+Jeder Fall eigenstaendig, jeder mit sprechendem Namen:
+
+BESTEHENDE dreizehn Faelle (`validateUser` LDAP und lokal,
+`requestPasswordReset`, `resetPassword`): auf den Nachbau umstellen, KEINEN
+loeschen; die drei Faelle mit "mandantengebunden" im Namen pruefen ab jetzt
+das PROTOKOLL (Modell, Methode, Mandant `t1`), nicht mehr eine Attrappe auf
+dem ungebundenen Objekt. NEU dazu, die Grenze in einem Fall: `validateUser`
+mit lokalem Kennwort — die Suche lief GENAU EINMAL ueber das ungebundene
+`$queryRaw` (Aufzeichnung der Attrappe), der `lastLoginAt`-Schreibzugriff
+GENAU EINMAL ueber den gebundenen Klienten unter dem Mandanten der
+Funktionszeile, und das Bindungshilfsmittel wurde genau einmal mit diesem
+Mandanten aufgerufen.
+
+`getMe(tenantId, userId)`:
+- eigener Mandant, lokaler Benutzer: liefert die oeffentlichen Felder,
+ `isLocalUser === true`, `hasAvatar` je nach `avatarPath`; die Schluessel
+ `passwordHash`, `ldapDn`, `avatarPath` fehlen in der Antwort (T-gbh-03);
+- eigener Mandant, LDAP-Benutzer (kein Hash, `ldapDn` gesetzt):
+ `isLocalUser === false`;
+- FREMDER Mandant (Klient unter `t2`, Zeile unter `t1`): `null`, kein
+ Fehler;
+- genau EIN gebundener Klient je Aufruf (Wachhund ueber
+ `vi.mocked(forTenant).mock.calls.length`, Muster `dashboard.service.spec.ts`
+ Zeile 545).
+
+`changePassword(tenantId, userId, currentPassword, newPassword, response)`:
+- eigener Mandant, richtiges aktuelles Kennwort (echter argon2-Hash wie im
+ bestehenden lokalen Fall): der gebundene `update` traegt einen neuen Hash
+ (per `argon2.verify` gegen das neue Kennwort geprueft) und
+ `mustChangePassword: false`; `jwtService.sign` wurde mit einer Nutzlast
+ aufgerufen, die `tenantId: 't1'` und `mustChangePassword: false` traegt;
+ `response.cookie` wurde mit `'session'`, dem signierten Wert und
+ `httpOnly: true` aufgerufen;
+- FREMDER Mandant: `UnauthorizedException`, Meldung woertlich
+ `User not found or has no local password`, KEIN Schreibzugriff, KEIN
+ Cookie;
+- falsches aktuelles Kennwort: `Current password is incorrect`, kein
+ Schreibzugriff;
+- LDAP-Benutzer (kein Hash): `UnauthorizedException`, kein Schreibzugriff;
+- genau EIN gebundener Klient je Aufruf (Suche und Schreiben auf demselben).
+
+`adminResetPassword(tenantId, callerRole, userId, newPassword, mustChangePassword)`:
+- eigener Mandant, Aufrufer ADMIN, Ziel USER: gebundener `update` mit neuem
+ Hash (per `argon2.verify` geprueft), `mustChangePassword` TRUE, wenn der
+ Parameter weggelassen wird (Vorgabe bleibt);
+- `mustChangePassword: false` wird durchgereicht;
+- FREMDER Mandant: `BadRequestException`, Meldung woertlich `User not found`,
+ KEIN Schreibzugriff — die Meldung nennt weder Halter noch Mandanten;
+- Aufrufer ADMIN, Ziel SUPER_ADMIN im SELBEN Mandanten: `ForbiddenException`,
+ KEIN Schreibzugriff (T-FH9-04);
+- Aufrufer SUPER_ADMIN, Ziel SUPER_ADMIN: gelingt;
+- genau EIN gebundener Klient je Aufruf.
+
+Die Zahl der Faelle wird am Ende ABGEZAEHLT und im SUMMARY mit der gezaehlten
+Zahl genannt.
+
+
+TEIL 1 — der Dienst. Stelle in `apps/api/src/auth/auth.service.ts` die drei
+Methoden um, je Methode EIN Klient in der Zuweisungsform, die
+`rls-access-inventory.spec.ts` erkennt und die die drei bestehenden Stellen
+dieser Datei vormachen: Konstante `tenantPrisma` aus dem Bindungshilfsmittel
+mit `this.prisma` und dem uebergebenen Mandanten, `as any`.
+
+- `getMe(tenantId: string, userId: string)`: der `findUnique` mit dem
+ unveraenderten `select` laeuft ueber `tenantPrisma`; Rueckgabeform,
+ `isLocalUser`/`hasAvatar`, das Wegfiltern von `passwordHash`/`ldapDn`/
+ `avatarPath` bleiben WORTGLEICH.
+- `changePassword(tenantId: string, userId: string, currentPassword, newPassword, response)`:
+ `findUnique` UND `update` auf demselben `tenantPrisma`; Meldungen,
+ argon2-Pruefung, Neu-Signieren des Cookies bleiben WORTGLEICH.
+- `adminResetPassword(tenantId: string, callerRole: Role, userId: string, newPassword: string, mustChangePassword: boolean = true)`:
+ `findUnique` UND `update` auf demselben `tenantPrisma`; nach dem Fund und
+ VOR dem Schreiben der neue Riegel: ist `user.role === Role.SUPER_ADMIN`
+ und `callerRole !== Role.SUPER_ADMIN`, `ForbiddenException` mit einer
+ Meldung, die die Regel nennt, aber keine Aussage ueber andere Benutzer
+ macht (T-FH9-04). Importiere `Role` aus `@prisma/client` und
+ `ForbiddenException` aus `@nestjs/common`. Die `BadRequestException('User not found')`
+ bleibt wortgleich — sie ist nach der Bindung fuer ein fremdmandantiges
+ Ziel die richtige Antwort und nennt nichts Fremdes.
+
+Die drei `$queryRaw`-Aufrufe der Anmeldefunktionen in `validateUser`,
+`requestPasswordReset`, `resetPassword` bleiben UNVERAENDERT auf `this.prisma`
+— sie sind die Grenze, nicht der Umbau (Befund H). Aendere an diesen drei
+Methoden NICHTS ausser Kommentaren.
+
+Schreibe drei Kopfkommentare (je Methode) und ergaenze den Klassen- oder
+Dateikopf: warum die drei Methoden binden (nach der Anmeldung, Mandant im
+signierten Sitzungsnachweis — 260911-fh9), woher der Mandant kommt (das
+Claim, nicht die umschaltbare Guard-Eigenschaft, mit Grund aus Befund C;
+fuer die oberste Rolle der gebundene Fan-out des Aufrufers), warum der
+Anmeldeweg auf den Funktionen bleibt (Befund B/H, Verweis auf die
+Migration), was Etappe 3 hier wieder aufmacht ((h4)(a), in zwei Saetzen:
+Mandant VOR der Suche, Funktionen mit zwei Gleichheitsbedingungen — enger,
+nicht weiter), und dass der SUPER_ADMIN-Riegel in `adminResetPassword` den
+Fall INNERHALB des Mandanten schliesst, waehrend der Schwesterweg im Ledger
+steht. Kommentare in dieser Datei duerfen den ungebundenen Zugriff auf das
+Benutzermodell NICHT woertlich nennen (der Kopfkommentar von `validateUser`
+macht mit "dot" vor, wie das geht) und keinen gebundenen `$queryRaw`
+woertlich — beide Gates zaehlen ueber die ganze Datei, Kommentare
+eingeschlossen.
+
+TEIL 2 — der Controller und das Modul. In `apps/api/src/auth/auth.controller.ts`:
+
+- `me(@CurrentUser() user)` reicht `user.tenantId` und `user.id` durch —
+ woertlich `this.authService.getMe(user.tenantId, user.id)`.
+- `changePassword` reicht `user.tenantId, user.id, dto.currentPassword, dto.newPassword, res`
+ durch.
+- `adminResetPassword` bekommt zusaetzlich `@CurrentUser() currentUser` und
+ verzweigt ueber einen privaten Helfer `resolveTargetTenantId(currentUser, userId)`
+ in der Form von `user.controller.ts` `resolveTargetUser`: ist die Rolle
+ `Role.SUPER_ADMIN`, wird `this.userService.findByIdForPlatformAdmin(userId)`
+ aufgerufen — nicht gefunden: `BadRequestException('User not found')`
+ (die heutige Meldung des Dienstes, damit ein API-Aufrufer denselben
+ Statuscode sieht wie bisher), sonst der `tenantId` des Ziels; jede andere
+ Rolle bekommt `currentUser.tenantId`. Danach
+ `this.authService.adminResetPassword(tenantId, currentUser.role, userId, dto.newPassword, dto.mustChangePassword ?? true)`.
+ `@Roles(Role.ADMIN, Role.SUPER_ADMIN)` und `@UseGuards(RolesGuard)` bleiben
+ WORTGLEICH. Der Controller liest an KEINER Stelle die vom Guard gesetzte
+ Anfrageobjekt-Kennung oder die SUPER_ADMIN-Kopfzeile (Befund C), und er
+ bekommt KEINEN direkten Datenbankzugang — die oberste Rolle geht ueber
+ `UserService`. Konstruktor: `authService` und `userService`.
+- Kopfkommentar des Handlers `adminResetPassword`: warum der Mandant aus dem
+ Sitzungsnachweis kommt und nicht aus Pfad, Rumpf oder Kopfzeile
+ (T-FH9-02), warum die oberste Rolle den Fan-out benutzt (Praezedenzfall
+ 260910-das, stille Funktionsminderung sonst), dass der Weg keinen
+ Frontend-Aufrufer hat (gemessen, Befund D) und der Schwesterweg
+ `PATCH /users/:id` ist. Kopfkommentar von `me`: warum das Claim und nicht
+ die Guard-Kennung (ein umgeschalteter SUPER_ADMIN muss sich selbst sehen).
+
+In `apps/api/src/auth/auth.module.ts`: `UserModule` zu `imports` — mit einem
+Kommentar, der die Zyklusfreiheit als Messung nennt (`UserModule` importiert
+`GroupsModule`, dieses nichts; kein Modul ausser `AppModule` importiert
+`AuthModule`) — die Anweisungen aus Befund D vor der Aenderung erneut
+ausfuehren.
+
+Falsifizierungsnachweise am Ende dieser Aufgabe, jeder zurueckgenommen und
+mit Testname und Fehlermeldung woertlich notiert: (a) ersetze in `getMe`
+probeweise den gebundenen Klienten durch den ungebundenen Basisclient —
+`auth.service.spec.ts` wird rot (erwartete Form: der Nachbau hat kein
+ungebundenes Benutzermodell); (b) verschiebe in `validateUser` probeweise die
+Anmeldesuche auf den gebundenen Klienten — der Grenz-Fall wird rot
+(erwartete Form: der gebundene Nachbau hat kein `$queryRaw`); (c) entferne
+probeweise den SUPER_ADMIN-Riegel in `adminResetPassword` — genau der
+`ForbiddenException`-Fall wird rot. Zaehle jeweils, wie viele Faelle rot
+wurden, und nenne die Zahl.
+
+Aendere keine Datei ausserhalb der vier genannten. Insbesondere: KEINE
+Aenderung am DTO, an den Strategien, am Guard, an `user.controller.ts`.
+
+
+ npm --prefix apps/api run test -- src/auth/auth.service.spec.ts && TOUT=$(npm --prefix apps/api run test 2>&1) && echo "$TOUT" | tail -6 && T=$(echo "$TOUT" | sed -nE 's/^ *Tests +([0-9]+) passed.*/\1/p' | head -1) && { test -n "$T" && test "$T" -gt 911 || { echo "TESTZAHL: ${T:-unbekannt}, erwartet mehr als 911 (neue Dienst-Faelle)"; exit 1; }; } && npm --prefix apps/api run type-check && F=apps/api/src/auth/auth.service.ts && SRC=$(grep -vE '^\s*(//|\*|/\*)' "$F") && test 0 -eq "$(grep -c 'this\.prisma\.user' "$F")" && test 0 -eq "$(grep -c 'tenantPrisma\.\$queryRaw' "$F")" && B=$(printf '%s\n' "$SRC" | grep -o 'tenantPrisma\.user\.' | wc -l | tr -d ' ') && { test "$B" -eq 8 || { echo "BINDUNG: $B gebundene Benutzerzugriffe in auth.service.ts, erwartet genau 8 (validateUser 2, resetPassword 1, getMe 1, changePassword 2, adminResetPassword 2)"; exit 1; }; } && R=$(printf '%s\n' "$SRC" | grep -o 'tenantPrisma\.passwordResetToken\.' | wc -l | tr -d ' ') && { test "$R" -eq 2 || { echo "RESET-TOKEN: $R gebundene Zugriffe, erwartet genau 2 (unveraendert)"; exit 1; }; } && C=$(printf '%s\n' "$SRC" | grep -o 'forTenant(this\.prisma' | wc -l | tr -d ' ') && { test "$C" -eq 6 || { echo "KLIENTEN: $C Aufrufstellen des Bindungshilfsmittels in auth.service.ts, erwartet genau 6 (drei bestehende plus getMe, changePassword, adminResetPassword)"; exit 1; }; } && Q=$(printf '%s\n' "$SRC" | grep -c 'this\.prisma\.\$queryRaw') && { test "$Q" -eq 3 || { echo "ANMELDESUCHE: $Q ungebundene queryRaw-Stellen, erwartet genau 3 (die Grenze bleibt)"; exit 1; }; } && L=$(printf '%s\n' "$SRC" | grep -c 'SELECT \* FROM auth_lookup_') && { test "$L" -eq 3 || { echo "ANMELDEFUNKTIONEN: $L Aufrufe, erwartet genau 3"; exit 1; }; } && grep -q 'async getMe(tenantId: string, userId: string)' "$F" && sed -n '/async changePassword(/,/): Promise {/p' "$F" | grep -q 'tenantId: string' && sed -n '/async adminResetPassword(/,/): Promise {/p' "$F" | grep -q 'tenantId: string' && sed -n '/async adminResetPassword(/,/): Promise {/p' "$F" | grep -q 'callerRole: Role' && for M in getMe changePassword adminResetPassword; do REG=$(awk -v m="async $M(" 'index($0,m){f=1} f{print} f&&/^ \}$/{f=0}' "$F" | grep -vE '^\s*(//|\*|/\*)'); K=$(printf '%s\n' "$REG" | grep -c 'forTenant(this\.prisma, tenantId)'); test "$K" -eq 1 || { echo "METHODE $M: $K Klienten, erwartet genau 1"; exit 1; }; U=$(printf '%s\n' "$REG" | grep -c 'this\.prisma\.'); test "$U" -eq 0 || { echo "METHODE $M: $U ungebundene Zugriffe (this.prisma.) neben dem Bindungsaufruf, erwartet 0"; exit 1; }; done && for M in validateUser requestPasswordReset resetPassword; do REG=$(awk -v m="async $M(" 'index($0,m){f=1} f{print} f&&/^ \}$/{f=0}' "$F" | grep -vE '^\s*(//|\*|/\*)'); Q1=$(printf '%s\n' "$REG" | grep -c 'this\.prisma\.\$queryRaw'); test "$Q1" -eq 1 || { echo "ANMELDEWEG $M: $Q1 ungebundene queryRaw-Stellen, erwartet genau 1"; exit 1; }; done && awk '/async adminResetPassword\(/{f=1} f&&/^ \}$/{f=0} f' "$F" | grep -q 'ForbiddenException' && awk '/async adminResetPassword\(/{f=1} f&&/^ \}$/{f=0} f' "$F" | grep -q 'Role.SUPER_ADMIN' && grep -q "import { Role } from '@prisma/client'" "$F" && grep -q '260911-fh9' "$F" && git diff --quiet 6236b30 -- apps/api/prisma/migrations/20260909160000_auth_lookup_functions && S=apps/api/src/auth/auth.service.spec.ts && test 0 -eq "$(grep -c 'forTenant: vi.fn((p' "$S")" && grep -q '__makeBoundClient' "$S" && grep -q 'vi.mocked(forTenant).mock.calls.length' "$S" && grep -q 'getMe' "$S" && grep -q 'changePassword' "$S" && grep -q 'adminResetPassword' "$S" && grep -q 'ForbiddenException' "$S" && grep -q 'User not found or has no local password' "$S" && grep -q 'P2025' "$S" && CT=apps/api/src/auth/auth.controller.ts && grep -q 'getMe(user.tenantId, user.id)' "$CT" && test 0 -eq "$(grep -c 'req.tenantId' "$CT")" && test 0 -eq "$(grep -c 'x-tenant-id' "$CT")" && test 0 -eq "$(grep -c 'PrismaService' "$CT")" && grep -q 'findByIdForPlatformAdmin' "$CT" && grep -q 'resolveTargetTenantId' "$CT" && grep -q "import { UserService } from '../user/user.service'" "$CT" && grep -q '@Roles(Role.ADMIN, Role.SUPER_ADMIN)' "$CT" && awk '/this\.authService\.changePassword\(/{f=1} f{print} f&&/\);/{f=0}' "$CT" | grep -q 'user.tenantId' && awk '/this\.authService\.adminResetPassword\(/{f=1} f{print} f&&/\);/{f=0}' "$CT" | grep -q 'currentUser.role' && grep -q '260911-fh9' "$CT" && grep -q 'UserModule' apps/api/src/auth/auth.module.ts && test 0 -eq "$(grep -rl 'AuthModule' apps/api/src --include=*.module.ts | grep -vE 'app\.module\.ts|auth\.module\.ts' | wc -l | tr -d ' ')" && git diff --quiet 6236b30 -- apps/api/src/auth/dto apps/api/src/auth/strategies apps/api/src/auth/guards apps/api/src/auth/decorators apps/api/src/user && test 0 -eq "$(grep -c 'tenantId' apps/api/src/auth/dto/admin-reset-password.dto.ts)" && git rev-parse --verify 6236b30 >/dev/null && PRISMA_CHANGED=$(git diff --name-only 6236b30 -- apps/api/prisma) && { test -z "$PRISMA_CHANGED" || { printf 'SCHEMA/MIGRATION GEAENDERT — in diesem Plan verboten:\n%s\n' "$PRISMA_CHANGED"; exit 1; }; } && CHANGED=$(git diff --name-only 6236b30) && UNEXPECTED=$(printf '%s\n' "$CHANGED" | grep -v '^$' | grep -vE '^(apps/api/scripts/rls-scratch-check\.mjs|docs/mandantentrennung-etappe2-fehlerrichtung\.md|apps/api/src/auth/auth\.service\.ts|apps/api/src/auth/auth\.service\.spec\.ts|apps/api/src/auth/auth\.controller\.ts|apps/api/src/auth/auth\.module\.ts|\.planning/.*)$' || true) && { test -z "$UNEXPECTED" || { printf 'UNERWARTETE AENDERUNGEN AUSSERHALB DES PLANUMFANGS:\n%s\n' "$UNEXPECTED"; exit 1; }; }
+
+ `auth.service.ts`: `getMe`, `changePassword`, `adminResetPassword` nehmen den Mandanten als ersten Parameter, laufen je ueber genau EINEN Klienten `tenantPrisma` (acht gebundene Benutzerzugriffe, sechs Aufrufstellen des Bindungshilfsmittels), `adminResetPassword` kennt die Rolle des Aufrufers und verweigert einem Nicht-SUPER_ADMIN das Kennwort eines SUPER_ADMIN; die drei `$queryRaw`-Anmeldesuchen stehen unveraendert auf dem ungebundenen Klienten, die Funktions-Migration ist unangetastet; kein ungebundener Benutzerzugriff mehr, auch nicht in Kommentaren; Kopfkommentare nennen Grenze, Mandantenquelle, Etappe-3-Vorbehalt. `auth.service.spec.ts` hat keine Identitaets-Attrappe mehr, zwei unterscheidbare Klienten (ungebunden ohne Modelle, gebunden ohne `$queryRaw`), alle bestehenden Faelle umgestellt, jeden in `` genannten Fall und den Wachhund. `auth.controller.ts` reicht fuer `me`/`changePassword` das Claim durch, verzweigt in `adminResetPassword` nach Rolle ueber `resolveTargetTenantId` mit `UserService.findByIdForPlatformAdmin` fuer die oberste Rolle, liest weder die Guard-Kennung noch die Kopfzeile und hat keinen Datenbankzugang. `auth.module.ts` importiert `UserModule`, zyklusfrei gemessen. Alle drei Falsifizierungsnachweise durchgefuehrt, zurueckgenommen, woertlich notiert. Baseline gehalten, Testzahl gestiegen, Typpruefung sauber, DTO/Strategien/Guards/Decorators/`user`-Bereich unveraendert.
+
+
+
+ Aufgabe 3: Die Mandantenquelle im Controller festnageln (neue Testdatei), alle Dokumentstellen der Klassifikation nachziehen, zwei Ledger-Eintraege anlegen, Gates falsifizieren
+ apps/api/src/auth/auth.controller.spec.ts, docs/mandantentrennung-zugriffsklassifikation.md, .planning/WINDOWS.md
+ apps/api/src/auth/auth.controller.ts (Stand nach Aufgabe 2), apps/api/src/tenant/tenant.controller.spec.ts (Attrappen, `reflect-metadata`, Rollen-Metadaten-Faelle), apps/api/src/auth/decorators/public.decorator.ts (`IS_PUBLIC_KEY`), apps/api/src/auth/decorators/roles.decorator.ts (`ROLES_KEY`), apps/api/src/prisma/rls-access-inventory.spec.ts (`analyzeFile`, `computeStandByKey`, `parseDocEntries`), docs/mandantentrennung-zugriffsklassifikation.md (VOLLSTAENDIG: Uebersichtstabelle samt Messanweisung und `auth`-Zeile, Summenzeile, Klassen-Verteilung, Hintergrunddienst-Abschnitt, beide `auth`-Zeilen der Bestandsaufnahme, `Was diese Etappe NICHT entscheidet`), docs/mandantentrennung-etappe2-fehlerrichtung.md (Abschnitt `## Bereich auth` aus Aufgabe 1, besonders (h3), (h4)(a), (h4)(b)), .planning/WINDOWS.md (Kopfzaehler, Eintraege #22, #26, #27 als Form), .planning/quick/260911-cwh-mandantentrennung-etappe-2-bereich-calen/260911-cwh-PLAN.md (Absatz zu `gsd-tools windows append`)
+
+ZUERST die Testlage. Lege `apps/api/src/auth/auth.controller.spec.ts` NEU an,
+in der Form von `tenant.controller.spec.ts`: `AuthService` und `UserService`
+als Attrappen (`vi.fn`), `new AuthController(authService, userService)`,
+`reflect-metadata` am Kopf. Jeder Fall eigenstaendig, jeder mit sprechendem
+Namen:
+
+- `me` mit `user = { id: 'u1', tenantId: 't1', role: 'USER' }`: `getMe`
+ wurde GENAU mit `('t1', 'u1')` aufgerufen — der Mandant ist das Claim.
+- `me` mit einem SUPER_ADMIN, dessen Claim `t1` traegt: ebenfalls `('t1', 'u1')`
+ — es gibt keine Kopfzeile und keine Guard-Kennung, die das aendern koennte,
+ weil der Handler nur `@CurrentUser()` liest.
+- `me`, wenn der Dienst `null` liefert: der Handler gibt `null` zurueck und
+ wirft NICHT — festgenagelt als das Verhalten, das (h3) beschreibt (der
+ leere Rumpf beginnt hier).
+- `changePassword`: `('t1', 'u1', dto.currentPassword, dto.newPassword, res)`,
+ Antwort `{ message: 'Password changed successfully.' }`.
+- `adminResetPassword`, Aufrufer ADMIN (`tenantId: 't1'`), Ziel `'target'`:
+ Dienst mit `('t1', 'ADMIN', 'target', 'new-password', true)` aufgerufen;
+ `userService.findByIdForPlatformAdmin` NICHT aufgerufen; Antwort
+ `{ message: 'User password has been reset.' }`.
+- `adminResetPassword`, Aufrufer ADMIN, `mustChangePassword: false` im Rumpf:
+ `false` wird durchgereicht.
+- `adminResetPassword`, Aufrufer SUPER_ADMIN (`tenantId: 't1'`), Fan-out
+ liefert `{ id: 'target', tenantId: 't9', role: 'USER' }`: Dienst mit
+ `('t9', 'SUPER_ADMIN', 'target', ...)` — der Mandant des ZIELS, nicht der
+ des Aufrufers; `findByIdForPlatformAdmin` genau einmal mit `'target'`.
+- `adminResetPassword`, Aufrufer SUPER_ADMIN, Fan-out liefert `null`:
+ `BadRequestException` mit Meldung `User not found`, Dienst NICHT
+ aufgerufen.
+- Rollen-Metadaten: `Reflect.getMetadata(ROLES_KEY, AuthController.prototype.adminResetPassword)`
+ ist genau `[Role.ADMIN, Role.SUPER_ADMIN]`; auf der Klasse und auf `me`,
+ `changePassword`, `logout`, `login`, `requestReset`, `resetPassword`
+ undefiniert.
+- Public-Metadaten: `Reflect.getMetadata(IS_PUBLIC_KEY, ...)` ist `true` fuer
+ `login`, `requestReset`, `resetPassword` (der Anmeldeweg) und undefiniert
+ fuer `me`, `changePassword`, `adminResetPassword`, `logout` — die Grenze
+ aus Befund B als Metadaten-Test: wird eine Nach-Anmeldungs-Methode je
+ `@Public()`, ist ihr Claim leer und die Bindung liefe ins Leere.
+
+Die Zahl der Faelle wird am Ende ABGEZAEHLT und im SUMMARY mit der gezaehlten
+Zahl genannt.
+
+
+TEIL 1 — die Testdatei aus ``, dann ein Falsifizierungsnachweis:
+lass `resolveTargetTenantId` in `auth.controller.ts` probeweise auch fuer
+SUPER_ADMIN `currentUser.tenantId` liefern — genau der Fan-out-Fall wird rot
+(erwartete Form: Dienst mit `t1` statt `t9` aufgerufen); Testname und Meldung
+woertlich notieren, Zustand wiederherstellen. Der Controller selbst wird in
+dieser Aufgabe NICHT dauerhaft veraendert.
+
+TEIL 2 — die Klassifikation. Ziehe `docs/mandantentrennung-zugriffsklassifikation.md`
+an ALLEN handgepflegten Stellen nach, jede einzeln nachgesehen, keine
+ueberflogen (Fehler 4 des Vorhabens):
+
+1. Uebersichtszeile `auth` mit den NEU GEMESSENEN Zahlen aus der im Dokument
+ genannten Messanweisung (beide Spalten), im etablierten Stil mit Vermerk
+ des vorherigen Standes (`**war 8/5**`): welche fuenf Zugriffe gebunden
+ wurden (drei Methoden), und dass die verbleibenden ungebundenen Rohtreffer
+ die `$queryRaw`-Aufrufe der drei Anmeldefunktionen sind — KEINE
+ Modellzugriffe (die Bestandsaufnahme fuehrt sie nicht, weil `$` kein
+ Modellname ist), bewusst und dauerhaft ungebunden, Verweis auf
+ `20260909160000_auth_lookup_functions` und (h1).
+2. Summenzeile derselben Tabelle mit fortgeschriebener Herkunftsspur
+ (ungebunden und gebunden je um die Differenz aus Schritt 1).
+3. Bestandsaufnahme-Zeile `apps/api/src/auth/auth.service.ts | user`:
+ Stand von `gemischt` auf `gebunden`, Begruendung NEU: die drei Methoden
+ binden an den Mandanten aus dem Sitzungsnachweis (260911-fh9); die drei
+ Anmeldesuchen laufen ueber die Funktionen und sind keine Modellzugriffe;
+ die oberste Rolle loest den Mandanten des Ziels ueber
+ `UserService.findByIdForPlatformAdmin` auf; der SUPER_ADMIN-Riegel;
+ Etappe-3-Vorbehalt in einem Satz. Die Zeile
+ `apps/api/src/auth/auth.service.ts | passwordResetToken` bleibt
+ UNVERAENDERT. Ohne Schritt 3 ist `rls-access-inventory.spec.ts` am Ende
+ dieser Aufgabe rot (Stand-Vergleich).
+4. Klassen-Verteilung: ein `**Stand 260911-fh9 (Aufgabe 3): unveraendert,
+ ausdruecklich festgehalten statt uebersprungen.**`-Absatz in der Form von
+ 260910-krx/260911-cwh — weiterhin 64 Paare, keine Klasse verschiebt sich,
+ nur die Stand-Spalte EINES Paares aendert sich. Tabelle und Ueberschrift
+ bleiben.
+5. Hintergrunddienst-Abschnitt: ein PLAIN-Absatz (kein Aufzaehlungspunkt,
+ keine Zeile der Form "Der ... Fall, anderer Bauart"), der festhaelt, dass
+ dieser Bereich keinen sechsten Fall hinzufuegt, mit den drei Anweisungen
+ aus Befund J, und dass die einzige Stelle, an der dieser Bereich ohne
+ Mandantenkontext liest, der Anmeldeweg ist — geloest durch Funktionen,
+ nicht durch die Bauform "uebergreifend lesen, dann je Mandant binden".
+6. Abschnitt `Was diese Etappe NICHT entscheidet`: ein NEUER Punkt — wie
+ der Anmeldeweg unter je Mandant eindeutigen Anmeldenamen (Etappe-3-
+ Entscheidung (1)) den Mandanten VOR der Benutzersuche erfaehrt; dass die
+ drei Funktionen dabei ENGER werden (zwei Gleichheitsbedingungen), nicht
+ weiter; dass die Bindung dieses Bereichs (Claim, `User.id`) davon
+ unberuehrt bleibt; Verweis auf (h4)(a) und `260911-fh9`.
+
+TEIL 3 — die Ledger-Eintraege, ueber `gsd-tools windows append` (damit
+Tabelle, JSON-Block und Kopfzaehler zusammenpassen — nicht von Hand), je mit
+`--kind`, `--phase quick-260911-fh9`, `--file`, `--description`:
+
+(1) `--kind deviation --file apps/web/src/components/layout/header.tsx`:
+die verschluckte Auspraegung der umgekehrten Fehlerrichtung im Bereich
+`auth` aus (h3): `getMe` liefert nach dem Scharfschalten `null`, der
+Controller antwortet 200 mit leerem Rumpf, `fetchCurrentUser`
+(`auth-actions.ts`) macht daraus `null`, `header.tsx` und
+`account-settings-form.tsx` (mit Stellen) tun bei `null` nichts — die
+Portalhuelle rendert ohne angemeldeten Benutzer, "nicht angemeldet" und
+"Zeile unsichtbar" sind fuer das Frontend derselbe Wert; dazu
+`changePassword` als `networkError`; die Etappe-4-Vorabpruefung aus (h4)(e);
+an dieselbe Bedingung gebunden wie #18; Familie #23/#25/#26; das Frontend
+wird von 260911-fh9 NICHT geaendert.
+
+(2) `--kind unmet-truth --file apps/api/src/user/user.controller.ts` — NUR,
+wenn Aufgabe 1 (E) die Luecke bestaetigt hat: ein ADMIN kann im eigenen
+Mandanten das Kennwort eines SUPER_ADMIN setzen (`PATCH /users/:id` mit
+`password`) und ihn deaktivieren/loeschen, weil T-02-08 nur das ZUWEISEN der
+Rolle SUPER_ADMIN prueft, nicht die Rolle des Ziels — mit den gelesenen
+Zeilen; kein Mandantenproblem, sondern Rechteausweitung innerhalb des
+Mandanten; die Reparatur in einem Satz (Rolle des Ziels in `update`,
+`deactivate`, `delete` pruefen — die Vorlage steht seit 260911-fh9 in
+`AuthService.adminResetPassword`); ausserhalb der Erlaubnisliste dieses
+Plans, deshalb Ledger statt Reparatur; vor dem ersten Mandanten mit einem
+zweiten Administrator zu schliessen.
+
+Pruefe nach dem Anlegen, dass jeder Eintrag in Tabelle UND JSON-Block steht
+und die Kopfzaehler (`open_count`, `total_count`) mit den Zeilen
+uebereinstimmen.
+
+TEIL 4 — zwei Falsifizierungsnachweise fuer die Dokument-Gates, jeder
+zurueckgenommen und mit Meldung woertlich notiert: (a) setze die
+Bestandsaufnahme-Zeile `auth.service.ts | user` probeweise zurueck auf
+`gemischt` — `rls-access-inventory.spec.ts` muss rot werden; (b) setze die
+Uebersichtszeile `auth` probeweise auf eine falsche Zahl — das herleitende
+Gate dieser Aufgabe muss fehlschlagen.
+
+Aendere keine Datei ausserhalb der drei genannten.
+
+
+ DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && SOUT=$(TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs) && echo "$SOUT" | tail -1 && N=$(echo "$SOUT" | sed -nE 's/^Alle ([0-9]+) Pruefungen bestanden\.$/\1/p') && { test -n "$N" && test "$N" -ge 120 || { echo "WERKZEUG: ${N:-nicht alle} Pruefungen bestanden, erwartet mindestens 120"; exit 1; }; } && test -f apps/api/src/auth/auth.controller.spec.ts && npm --prefix apps/api run test -- src/auth/auth.controller.spec.ts && npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts && TOUT=$(npm --prefix apps/api run test 2>&1) && echo "$TOUT" | tail -6 && T=$(echo "$TOUT" | sed -nE 's/^ *Tests +([0-9]+) passed.*/\1/p' | head -1) && { test -n "$T" && test "$T" -gt 911 || { echo "TESTZAHL: ${T:-unbekannt}, erwartet mehr als 911"; exit 1; }; } && TF=$(echo "$TOUT" | sed -nE 's/^ *Test Files +([0-9]+) passed.*/\1/p' | head -1) && { test -n "$TF" && test "$TF" -ge 60 || { echo "TESTDATEIEN: ${TF:-unbekannt}, erwartet mindestens 60 (59 plus auth.controller.spec.ts)"; exit 1; }; } && npm --prefix apps/api run type-check && CS=apps/api/src/auth/auth.controller.spec.ts && grep -q 'reflect-metadata' "$CS" && grep -q 'ROLES_KEY' "$CS" && grep -q 'IS_PUBLIC_KEY' "$CS" && grep -q 'findByIdForPlatformAdmin' "$CS" && grep -q "'t9'" "$CS" && grep -q 'User not found' "$CS" && grep -q 'toBeNull\|toBe(null)' "$CS" && { test -z "$(git status --porcelain -- apps/api/src/auth/auth.controller.ts)" || { echo "auth.controller.ts hat in Aufgabe 3 uncommittete Aenderungen — der Falsifizierungsnachweis wurde nicht zurueckgenommen"; exit 1; }; } && DU=$(grep -ro "this\.prisma\.[a-zA-Z]*" apps/api/src/auth | grep -v spec | wc -l | tr -d ' ') && DB=$(grep -ro "tenantPrisma\.[a-zA-Z]*\." apps/api/src/auth | grep -v spec | wc -l | tr -d ' ') && { test "$DU" -eq 3 || { echo "UEBERSICHTSZEILE: ungebundene Rohtreffer in apps/api/src/auth sind $DU, erwartet 3 (die drei Anmeldesuchen)"; exit 1; }; } && { test "$DB" -eq 10 || { echo "UEBERSICHTSZEILE: gebundene Rohtreffer in apps/api/src/auth sind $DB, erwartet 10"; exit 1; }; } && { grep -qE "^\| auth \| ${DU} \| ${DB} \| \*\*war 8/5\*\*" docs/mandantentrennung-zugriffsklassifikation.md || { echo "UEBERSICHTSZEILE auth nennt nicht die neu gemessenen Zahlen ${DU}/${DB} im etablierten Stil"; exit 1; }; } && grep -E "^\| auth \| " docs/mandantentrennung-zugriffsklassifikation.md | grep -q '20260909160000' && awk -F'|' '$2 ~ /^ *[a-z][a-z-]* *$/ && $3 ~ /^ *[0-9]+ *$/ && $4 ~ /^ *[0-9]+ *$/ { su+=$3; sb+=$4; n++ } $2 ~ /^ *\*\*Summe\*\* *$/ && $4 !~ /^ *$/ { g3=$3; g4=$4; gsub(/[^0-9]/,"",g3); gsub(/[^0-9]/,"",g4); ru=g3+0; rb=g4+0; seen=1 } END { if (!seen || n+0 != 12) { print "UEBERSICHTSTABELLE nicht erkannt, Bereichszeilen: " n; exit 1 } if (su+0 != ru || sb+0 != rb) { print "SUMMENZEILE stimmt nicht: Bereichszeilen ergeben " su "/" sb ", Summenzeile nennt " ru "/" rb; exit 1 } }' docs/mandantentrennung-zugriffsklassifikation.md && awk -F'|' '$2 ~ /^ *apps\/api\/src\// { k=$4; gsub(/^ +| +$/,"",k); cls[k]++; pairs++ } $2 ~ /^ *(muss-mandantengebunden|keine-mandantengebundene-tabelle|beides|bewusst-uebergreifend) *$/ && $3 ~ /^ *[0-9]+ *$/ && $4 ~ /^ *$/ { k=$2; gsub(/^ +| +$/,"",k); v=$3; gsub(/[^0-9]/,"",v); tab[k]=v+0; tn++ } $2 ~ /^ *\*\*Summe\*\* *$/ && $4 ~ /^ *$/ { v=$3; gsub(/[^0-9]/,"",v); tsum=v+0; tseen=1 } /^## Klassen-Verteilung/ { h=$0; gsub(/[^0-9]/,"",h); hp=h+0; hseen=1 } END { if (tn+0 != 4 || !tseen || !hseen) { print "KLASSEN-VERTEILUNG nicht erkannt: Klassenzeilen " tn ", Summenzeile " tseen ", Ueberschrift " hseen; exit 1 } if (tsum != pairs+0) { print "KLASSEN-SUMME stimmt nicht: Bestandsaufnahme hat " pairs " Paare, Tabellensumme nennt " tsum; exit 1 } if (hp != pairs+0) { print "UEBERSCHRIFT der Klassen-Verteilung nennt " hp " Paare, Bestandsaufnahme hat " pairs; exit 1 } s=0; for (k in tab) { if (tab[k] != cls[k]+0) { print "KLASSE " k ": Tabelle nennt " tab[k] ", Bestandsaufnahme zaehlt " cls[k]+0; exit 1 } s+=tab[k] } if (s != pairs+0) { print "KLASSENZEILEN ergeben " s ", Bestandsaufnahme hat " pairs; exit 1 } }' docs/mandantentrennung-zugriffsklassifikation.md && grep -qE '^\| apps/api/src/auth/auth\.service\.ts \| user \| muss-mandantengebunden \| gebunden \|.*260911-fh9' docs/mandantentrennung-zugriffsklassifikation.md && grep -qE '^\| apps/api/src/auth/auth\.service\.ts \| passwordResetToken \| muss-mandantengebunden \| gebunden \|' docs/mandantentrennung-zugriffsklassifikation.md && test 0 -eq "$(grep -cE '^\| apps/api/src/auth/auth\.service\.ts \| [a-zA-Z]+ \| [a-z-]+ \| gemischt \|' docs/mandantentrennung-zugriffsklassifikation.md)" && awk '/^## Klassen-Verteilung/{f=1; next} /^## /{f=0} f && /Stand 260911-fh9/{m=1} END{ if(!m){print "KLASSEN-VERTEILUNG: kein Stand-Vermerk fuer 260911-fh9"; exit 1} }' docs/mandantentrennung-zugriffsklassifikation.md && awk 'BEGIN{split("ein zwei drei vier x sechs sieben acht neun",w," "); w[5]="fünf"} /^## Der Hintergrunddienst als Falle/{seen=1; head=$0; f=1; next} /^## /{f=0} f && /^- \*\*`/{n++} f && /^\*\*Der .* Fall, anderer Bauart/{n++} f && /260911-fh9/{m=1} END{ if(!seen){print "ABSCHNITT Hintergrunddienst nicht gefunden"; exit 1} want="## Der Hintergrunddienst als Falle — " w[n] " Fälle"; if(head != want){printf "HINTERGRUNDDIENST-UEBERSCHRIFT nennt \"%s\", gezaehlt wurden %d Faelle, erwartet \"%s\"\n", head, n, want; exit 1} if(!m){print "ABSCHNITT Hintergrunddienst nennt 260911-fh9 nicht — die Abwesenheit eines sechsten Falls ist nicht belegt"; exit 1} }' docs/mandantentrennung-zugriffsklassifikation.md && awk '/^## Was diese Etappe NICHT entscheidet/{f=1; next} f && /260911-fh9/{m=1} f && /Etappe 3|Etappe-3/{e=1} END{ if(!m||!e){print "ABSCHNITT \"Was diese Etappe NICHT entscheidet\": der Etappe-3-Punkt zum Anmeldeweg (260911-fh9) fehlt"; exit 1} }' docs/mandantentrennung-zugriffsklassifikation.md && W=.planning/WINDOWS.md && FH=$(grep -cE '^\| [0-9]+ \| quick-260911-fh9 \|' "$W") && { test "$FH" -ge 1 || { echo "LEDGER: kein Eintrag fuer quick-260911-fh9 in der Tabelle"; exit 1; }; } && grep -q 'header.tsx' "$W" && OC=$(sed -nE 's/^open_count: ([0-9]+)$/\1/p' "$W") && OR=$(grep -cE '^\| [0-9]+ \|.*\| open \|' "$W") && { test "$OC" -eq "$OR" || { echo "LEDGER: open_count=$OC, offene Tabellenzeilen=$OR"; exit 1; }; } && TC=$(sed -nE 's/^total_count: ([0-9]+)$/\1/p' "$W") && TR=$(grep -cE '^\| [0-9]+ \| ' "$W") && { test "$TC" -eq "$TR" || { echo "LEDGER: total_count=$TC, Tabellenzeilen=$TR"; exit 1; }; } && JC=$(grep -c '"phase": "quick-260911-fh9"' "$W") && { test "$JC" -eq "$FH" || { echo "LEDGER: $FH Tabellenzeilen, aber $JC JSON-Eintraege fuer quick-260911-fh9"; exit 1; }; } && git rev-parse --verify 6236b30 >/dev/null && PRISMA_CHANGED=$(git diff --name-only 6236b30 -- apps/api/prisma) && { test -z "$PRISMA_CHANGED" || { printf 'SCHEMA/MIGRATION GEAENDERT — in diesem Plan verboten:\n%s\n' "$PRISMA_CHANGED"; exit 1; }; } && WEB_CHANGED=$(git diff --name-only 6236b30 -- apps/web docker-compose.yml docker-compose.prod.yml) && { test -z "$WEB_CHANGED" || { printf 'FRONTEND/COMPOSE GEAENDERT — in diesem Plan verboten:\n%s\n' "$WEB_CHANGED"; exit 1; }; } && CHANGED=$(git diff --name-only 6236b30) && UNEXPECTED=$(printf '%s\n' "$CHANGED" | grep -v '^$' | grep -vE '^(apps/api/scripts/rls-scratch-check\.mjs|docs/mandantentrennung-etappe2-fehlerrichtung\.md|apps/api/src/auth/auth\.service\.ts|apps/api/src/auth/auth\.service\.spec\.ts|apps/api/src/auth/auth\.controller\.ts|apps/api/src/auth/auth\.module\.ts|apps/api/src/auth/auth\.controller\.spec\.ts|docs/mandantentrennung-zugriffsklassifikation\.md|\.planning/.*)$' || true) && { test -z "$UNEXPECTED" || { printf 'UNERWARTETE AENDERUNGEN AUSSERHALB DES PLANUMFANGS:\n%s\n' "$UNEXPECTED"; exit 1; }; }
+
+ `auth.controller.spec.ts` existiert neu und deckt jeden in `` genannten Fall ab — Mandantenquelle je Handler (das Claim; fuer die oberste Rolle der Mandant des Ziels aus dem Fan-out), unbekanntes Ziel, `null`-Durchreichung von `me`, Rollen-Metadaten und Public-Metadaten fuer alle sieben Handler; der Falsifizierungsnachweis am Fan-out-Zweig ist durchgefuehrt, zurueckgenommen und woertlich notiert, `auth.controller.ts` ist gegenueber Aufgabe 2 unveraendert. Alle handgepflegten Stellen von `docs/mandantentrennung-zugriffsklassifikation.md` sind nachgezogen und maschinell gegatet: Uebersichtszeile `auth` mit neu gemessenen Zahlen und Verweis auf die Funktions-Migration, Summenzeile, Bestandsaufnahme-Zeile `user` auf `gebunden` (keine `gemischt`-Zeile mehr fuer diese Datei), `passwordResetToken` unveraendert, Klassen-Verteilung mit 64 Paaren und Stand-Vermerk, Hintergrunddienst-Abschnitt ohne sechsten Fall, `Was diese Etappe NICHT entscheidet` mit dem Etappe-3-Punkt. `.planning/WINDOWS.md` traegt die neuen offenen Eintraege in Tabelle und JSON-Block, die Kopfzaehler stimmen. Beide Dokument-Falsifizierungen durchgefuehrt, zurueckgenommen, woertlich notiert. Werkzeug alle Pruefungen bestanden (mindestens 120), Baseline gehalten, Testzahl und Testdateizahl gestiegen, Typpruefung sauber, Schalter unveraendert aus, Funktionen unangetastet.
+
+
+
+
+
+
+Konfiguriert: ASVS-Stufe 1, blockierend ab `high`.
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| Unangemeldeter Browser -> Anmeldeweg (`POST /auth/login`, `/auth/request-reset`, `/auth/reset-password`) | Der Mandant ist VOR der Suche unbekannt; die Suche laeuft ueber drei SECURITY-DEFINER-Funktionen mit festem Spaltensatz, Gleichheitsvergleich und LIMIT 1 (20260909160000). Dieser Plan aendert daran NICHTS und misst das (Pruefung 1/3). |
+| Angemeldeter Benutzer -> Nach-Anmeldung (`GET /auth/me`, `POST /auth/change-password`) | Der Mandant ist das signierte Claim `tenantId` aus `JwtStrategy.validate`; die eigene Zeile liegt im eigenen Mandanten. `req.tenantId` (Guard, fuer SUPER_ADMIN per Kopfzeile umschaltbar) ist hier die FALSCHE Quelle. |
+| ADMIN / SUPER_ADMIN -> `POST /auth/admin-reset-password/:userId` | Die Kennung im Pfad ist frei waehlbare Eingabe und bezeichnet das ZIEL; der Mandant kommt fuer ADMIN aus dem eigenen Claim, fuer SUPER_ADMIN aus dem gebundenen Fan-out ueber die Kennung. Kein Aufrufer im Frontend (gemessen). |
+| ADMIN -> SUPER_ADMIN desselben Mandanten | Rollengrenze INNERHALB des Mandanten: heute in beiden Wegen (`admin-reset-password`, `PATCH /users/:id`) nicht geprueft. |
+| API -> PostgreSQL, Tabelle `User` | Regel `"tenantId" = current_tenant_id()` (20260618112133); nach dem Scharfschalten liefert ein ungebundener Zugriff null Zeilen (gemessen, Pruefung 4/7). |
+| API -> `auth_lookup_*`-Funktionen | Die schmale Ausnahme; EXECUTE nur fuer `tessera_app`; nichts in diesem Plan weitet sie (Pruefung 1/3, `git diff` auf die Migration leer). |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
+|-----------|----------|-----------|----------|-------------|-----------------|
+| T-FH9-01 | Elevation of Privilege | `auth.controller.ts`/`auth.service.ts` `adminResetPassword`, Mandantengrenze | high | mitigate | Ein ADMIN von Mandant A setzt das Kennwort eines Benutzers von Mandant B (heute: `findUnique` ueber die Kennung, ungebunden, kein Mandantenvergleich). Gebundener Klient unter dem Mandanten aus dem Sitzungsnachweis: die fremde Zeile ist unsichtbar (Pruefung 6/9 ueber den generierten Client), Antwort bleibt die bestehende `400 User not found` ohne Aussage ueber den fremden Mandanten. Testfaelle im Dienst (fremder Mandant: kein Schreibzugriff) und im Controller (Mandant = Claim); Falsifizierung durch Rueckbau. |
+| T-FH9-02 | Spoofing | `auth.controller.ts`, Herkunft des Mandanten | high | mitigate | Der Mandant koennte aus Pfad, Rumpf oder Kopfzeile kommen. Der Controller liest ausschliesslich `@CurrentUser()` (signiertes Claim) und fuer SUPER_ADMIN das Ergebnis des gebundenen Fan-outs; das DTO hat kein Mandantenfeld (gegatet); `req.tenantId` und `x-tenant-id` kommen in der Datei nicht vor (gegatet auf null); Controller-Tests nageln je Handler die uebergebene Mandantenkennung fest. |
+| T-FH9-03 | Information Disclosure | `auth_lookup_*`-Funktionen, Migration 20260909160000 | high | mitigate | Jede Ausweitung dessen, was die Funktionen preisgeben, oeffnete den Anmeldeweg zur Tabelle. Migration unangetastet (`git diff` gegatet), die drei `$queryRaw`-Stellen bleiben auf dem ungebundenen Klienten (gegatet: genau 3), `pg_proc` misst SECURITY DEFINER/STABLE/Suchpfad/LIMIT 1 (Pruefung 1), der feste Spaltensatz laesst die fuenf neuen Wegwerf-Spalten nicht durch (Pruefung 3). |
+| T-FH9-04 | Elevation of Privilege | `auth.service.ts` `adminResetPassword`, Rollengrenze im Mandanten | high | mitigate | Ein ADMIN setzt das Kennwort eines SUPER_ADMIN im eigenen Mandanten und meldet sich als Plattform-Administrator an. Neuer Riegel nach dem gebundenen Fund: Ziel SUPER_ADMIN und Aufrufer nicht SUPER_ADMIN -> `ForbiddenException`; Testfaelle beide Richtungen; Falsifizierung durch Entfernen des Riegels. |
+| T-FH9-05 | Elevation of Privilege | `user.controller.ts` `update`/`deactivate`/`delete` (Schwesterweg) | high | transfer | Derselbe Fall wie T-FH9-04 ueber `PATCH /users/:id` mit `password` (T-02-08 prueft nur das Zuweisen der Rolle, nicht die Rolle des Ziels). Ausserhalb der Erlaubnisliste dieses Plans; in Aufgabe 1 (E) gelesen, in (h4)(b) festgehalten, in Aufgabe 3 als OFFENER Ledger-Eintrag mit konkreter Reparatur uebergeben (die Vorlage steht in `adminResetPassword`). Kein Mandantenproblem. |
+| T-FH9-06 | Denial of Service | `getMe`/`changePassword` unter `x-tenant-id` | medium | mitigate | Waeren die Selbstbedienungswege an `req.tenantId` gebunden, saehe ein SUPER_ADMIN, der gerade einen fremden Mandanten ansieht, sich selbst nicht mehr (Portalhuelle ohne Benutzer, Kennwortwechsel unmoeglich). Bindung an das Claim; Controller-Test mit SUPER_ADMIN belegt, dass nur das Claim durchgereicht wird. |
+| T-FH9-07 | Repudiation / Information Disclosure | `getMe` -> Frontend, `changePassword` -> `auth-actions.ts` | medium | accept | Die umgekehrte Fehlerrichtung ist NICHT laut: `null` wird zur Portalhuelle ohne Benutzer, `401` wird zu `networkError`. Gemessen (Pruefung 4), in (h2)/(h3) beschrieben, als Ledger-Eintrag (Familie #23/#25/#26) festgehalten, Etappe-4-Vorabpruefung benannt. Das Frontend wird nicht geaendert (Umfang). |
+| T-FH9-08 | Tampering | Etappe-3-Umbau des Anmeldewegs | low | accept | Dieser Plan koennte Entscheidungen treffen, die den Umbau auf je Mandant eindeutige Anmeldenamen erschweren. Er bindet ausschliesslich an Claim und `User.id` (plattformweite UUID) und veraendert die Funktionen nicht; was Etappe 3 wieder aufmacht, steht in (h4)(a) und im Klassifikationsdokument. |
+| T-FH9-09 | Information Disclosure | `changePassword`, zwei 401-Meldungen | low | accept | `User not found or has no local password` vs. `Current password is incorrect` unterscheidet LDAP- von lokalen Konten fuer den ANGEMELDETEN Benutzer selbst — bestehendes Verhalten, betrifft nur die eigene Zeile, unveraendert; in (h4)(f) festgehalten. |
+| T-FH9-10 | Spoofing | `x-tenant-id` ohne Existenzpruefung | low | accept | Bekannt aus (n4)(f); hier nur relevant, weil es die Entscheidung gegen `req.tenantId` fuer Selbstbedienung stuetzt. Nicht dieser Auftrag. |
+| T-FH9-11 | Denial of Service | `AuthModule` importiert `UserModule` | low | accept | Ein Modulzyklus braeche den Start. Gemessen zyklusfrei (UserModule -> GroupsModule -> nichts; kein Modul ausser AppModule importiert AuthModule), gegatet; Typpruefung ueber den gesamten API-Quelltext. |
+| T-FH9-SC | Tampering | Paketinstallation | low | accept | Dieser Plan installiert kein Paket (npm/pip/cargo) und fuegt keine Abhaengigkeit hinzu. Das Legitimitaets-Gate faellt nicht an; ausdruecklich festgehalten statt schweigend ausgelassen. |
+
+
+
+
+
+Nach Abschluss aller drei Aufgaben:
+
+1. `node apps/api/scripts/rls-scratch-check.mjs` meldet alle Pruefungen
+ bestanden (110 bisherige plus die neuen, mindestens 120), Rueckgabewert 0;
+ `runAuthAreaChecks` steht nach `runTenantAreaChecks` und vor
+ `runTransactionShapeMeasurement`.
+2. `npm --prefix apps/api run test` meldet mehr als 911 Tests gruen in
+ mindestens 60 Dateien (59 bisherige plus `auth.controller.spec.ts`).
+3. `npm --prefix apps/api run type-check` ist sauber.
+4. `npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts`
+ ist gruen — 64 Paare, `auth.service.ts`/`user` steht auf `gebunden`, kein
+ `gemischt`-Paar mehr fuer diese Datei.
+5. In `auth.service.ts`: kein ungebundener Benutzerzugriff (auch nicht in
+ Kommentaren), acht gebundene Benutzerzugriffe, sechs Aufrufstellen des
+ Bindungshilfsmittels, genau drei `$queryRaw`-Anmeldesuchen auf dem
+ ungebundenen Klienten, kein gebundener `$queryRaw`. In
+ `auth.controller.ts`: kein `req.tenantId`, kein `x-tenant-id`, kein
+ direkter Datenbankzugang, `getMe(user.tenantId, user.id)`,
+ `findByIdForPlatformAdmin` fuer die oberste Rolle.
+6. `git diff --name-only 6236b30 -- apps/api/prisma` ist leer — Schema,
+ Migrationen und die drei Anmeldefunktionen unangetastet.
+7. Alle handgepflegten Stellen der Klassifikation sind maschinell gegatet und
+ gruen, die Zaehlgates LEITEN ihre Werte aus den im Dokument genannten
+ Messanweisungen ab.
+8. Der Umfang ist als ERLAUBNISLISTE gegatet: jede Datei, die sich gegenueber
+ `6236b30` geaendert hat, ist eine der neun in `files_modified` genannten
+ (oder liegt unter `.planning/`). Unter `apps/api/prisma`, `apps/web`,
+ `apps/api/src/user`, `apps/api/src/auth/dto|strategies|guards|decorators`
+ und den Compose-/Umgebungsdateien hat sich nichts geaendert.
+9. `.planning/WINDOWS.md`: neue offene Eintraege in Tabelle UND JSON-Block,
+ Kopfzaehler stimmen mit den Zeilen ueberein.
+10. `DATABASE_URL` zeigt unveraendert auf die Rolle `tessera`; nichts in
+ Active Directory.
+
+
+
+
+
+- Die Grenze zwischen Anmeldeweg und Nach-Anmeldung ist gelesen, gemessen
+ (Werkzeug UND Test) und festgenagelt: drei Funktionen fuer die Suche vor
+ bekanntem Mandanten, unveraendert; drei gebundene Methoden danach.
+- `getMe`, `changePassword`, `adminResetPassword` binden je ueber EINEN
+ Klienten `tenantPrisma` an den Mandanten aus dem signierten
+ Sitzungsnachweis; jeder Aufrufer im Controller ist umgestellt; die oberste
+ Rolle behaelt ihre Reichweite ueber den gebundenen Fan-out.
+- Die Rechteausweitung ueber die Mandantengrenze (T-FH9-01) und die
+ innerhalb des Mandanten in diesem Handler (T-FH9-04) sind geschlossen,
+ gemessen und getestet; die im Schwesterweg ist gelesen und im Ledger.
+- Die umgekehrte Fehlerrichtung ist in ihrer TATSAECHLICHEN Auspraegung
+ benannt (verschluckt, nicht laut; `networkError`, nicht "falsches
+ Kennwort") — mit jedem Glied der Kette, als Ledger-Eintrag, mit
+ Etappe-4-Vorabpruefung.
+- Etappe 3 ist nicht schwerer geworden; was sie wieder aufmacht, steht an
+ zwei Stellen.
+- Die Testlage hat keine Identitaets-Attrappe mehr; sieben
+ Falsifizierungsnachweise (drei in Aufgabe 2, eine am Controller, zwei an
+ den Dokument-Gates in Aufgabe 3, dazu die Lesung des Schwesterwegs) sind
+ durchgefuehrt, zurueckgenommen und woertlich notiert.
+- Fuenf handgepflegte Dokumentstellen nachgezogen und deriviert gegatet;
+ Erlaubnisliste gegen `6236b30`; Baseline gehalten nach jeder Aufgabe;
+ Schalter aus; Schema, Migrationen, Funktionen, Frontend, Active Directory
+ unberuehrt.
+
+
+
+
+