feat(260911-fh9): getMe/changePassword/adminResetPassword an den Mandanten aus dem Sitzungsnachweis binden

- auth.service.ts: die drei Nach-Anmeldungs-Methoden binden je ueber genau
  einen Klienten tenantPrisma; adminResetPassword verweigert einem
  Nicht-SUPER_ADMIN das Kennwort eines SUPER_ADMIN (T-FH9-04); die drei
  $queryRaw-Anmeldesuchen bleiben unveraendert auf dem ungebundenen Klienten
- auth.controller.ts: me/changePassword reichen user.tenantId aus dem Claim
  durch; adminResetPassword verzweigt ueber resolveTargetTenantId nach Rolle
  (ADMIN: eigener Mandant; SUPER_ADMIN: gebundener Fan-out
  UserService.findByIdForPlatformAdmin) — schliesst die Rechteausweitung
  ueber die Mandantengrenze (T-FH9-01)
- auth.module.ts: importiert UserModule, zyklusfrei gemessen
- auth.service.spec.ts: Identitaets-Attrappe ersetzt durch zwei
  unterscheidbare Klienten (__makeBoundClient); 29 Faelle, drei
  Falsifizierungsnachweise durchgefuehrt und zurueckgenommen

Bekannt und erwartet: rls-access-inventory.spec.ts ist nach diesem Commit
kurzzeitig rot (Bestandsaufnahme-Zeile auth.service.ts/user zeigt noch
"gemischt", gemessen ist jetzt "gebunden") — wird in Aufgabe 3 desselben
Plans geschlossen (927/928 Tests gruen, ein bekannter, in Aufgabe 3
behobener Fehlschlag, kein neuer).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
2026-09-11 11:56:14 +02:00
parent 9782bea1b2
commit 92aa8c403b
4 changed files with 624 additions and 48 deletions
+48 -2
View File
@@ -1,4 +1,5 @@
import {
BadRequestException,
Body,
Controller,
Get,
@@ -12,6 +13,7 @@ import {
import { AuthGuard } from '@nestjs/passport';
import { Role } from '@prisma/client';
import { Request, Response } from 'express';
import { UserService } from '../user/user.service';
import { AuthService } from './auth.service';
import { CurrentUser } from './decorators/current-user.decorator';
import { Public } from './decorators/public.decorator';
@@ -23,7 +25,33 @@ import { RolesGuard } from './guards/roles.guard';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
constructor(
private authService: AuthService,
private userService: UserService,
) {}
/**
* Loest den Mandanten fuer `adminResetPassword` auf (260911-fh9,
* Praezedenzfall `user.controller.ts` `resolveTargetUser`, 260910-das):
* ein ADMIN wirkt auf seinen EIGENEN Mandanten (Claim), die oberste
* Rolle (SUPER_ADMIN) behaelt ihre uebergreifende Reichweite ueber den
* gebundenen Fan-out `UserService.findByIdForPlatformAdmin` — sonst
* saehe ein SUPER_ADMIN nur noch den eigenen Mandanten, eine stille
* Funktionsminderung (Befund D). Nicht gefunden: dieselbe
* `BadRequestException('User not found')`, die der Dienst bisher ohne
* Mandantenpruefung warf, damit ein API-Aufrufer denselben Statuscode
* sieht wie vor dieser Umstellung.
*/
private async resolveTargetTenantId(currentUser: any, userId: string): Promise<string> {
if (currentUser.role === Role.SUPER_ADMIN) {
const target = await this.userService.findByIdForPlatformAdmin(userId);
if (!target) {
throw new BadRequestException('User not found');
}
return target.tenantId;
}
return currentUser.tenantId;
}
/**
* POST /auth/login
@@ -55,10 +83,16 @@ export class AuthController {
* GET /auth/me
* Returns enriched user profile: public fields + isLocalUser + hasAvatar.
* T-gbh-03: passwordHash and ldapDn are never serialised in the response.
*
* Mandant kommt ausschliesslich aus dem Sitzungsnachweis (`@CurrentUser()`,
* das Claim), NICHT aus der Anfrageobjekt-Eigenschaft, die `TenantGuard`
* fuer die oberste Rolle per Kopfzeile umschaltbar macht — ein
* umgeschalteter SUPER_ADMIN muss sich selbst weiterhin sehen (260911-fh9,
* Befund C).
*/
@Get('me')
async me(@CurrentUser() user: any) {
return this.authService.getMe(user.id);
return this.authService.getMe(user.tenantId, user.id);
}
/**
@@ -101,6 +135,7 @@ export class AuthController {
@Res({ passthrough: true }) res: Response,
) {
await this.authService.changePassword(
user.tenantId,
user.id,
dto.currentPassword,
dto.newPassword,
@@ -113,6 +148,13 @@ export class AuthController {
* POST /auth/admin-reset-password/:userId
* Admin resets a user's password (D-03 admin reset).
* T-02-15: Only ADMIN/SUPER_ADMIN via RolesGuard.
*
* Der Mandant des Ziels kommt ausschliesslich aus dem Sitzungsnachweis
* des AUFRUFERS bzw. aus dem gebundenen Fan-out fuer die oberste Rolle
* (`resolveTargetTenantId` oben) — NICHT aus Pfad, Rumpf oder Kopfzeile
* (T-FH9-02). `AdminResetPasswordDto` traegt bewusst kein Mandantenfeld.
* Kein Frontend-Aufrufer (gemessen, 260911-fh9 Befund D); der
* Schwesterweg ist `PATCH /users/:id`.
*/
@Post('admin-reset-password/:userId')
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
@@ -121,8 +163,12 @@ export class AuthController {
async adminResetPassword(
@Param('userId') userId: string,
@Body() dto: AdminResetPasswordDto,
@CurrentUser() currentUser: any,
) {
const tenantId = await this.resolveTargetTenantId(currentUser, userId);
await this.authService.adminResetPassword(
tenantId,
currentUser.role,
userId,
dto.newPassword,
dto.mustChangePassword ?? true,