f2fc39f51c
apps/api/src/auth/types/auth-user.ts angelegt: AuthUser, AuthenticatedRequest, LocalAuthenticatedRequest, LoginUser, JwtPayload, UploadedFileLike. Jedes Feld traegt seine Herkunft als Kommentar. tenantId ist string, hergeleitet und nicht gewaehlt: die Spalte User.tenantId ist in schema.prisma Pflicht, beide Signierstellen schreiben genau sie, und der Bestand beschreibt dasselbe Objekt in SessionUser schon so. Der SUPER_ADMIN-Zweig in TenantGuard spricht nicht dagegen - der Waechter liest AuthUser gar nicht, und dass es den Zweig gibt, steht als null in AuthenticatedRequest.tenantId weiter im Typsystem. tenant.guard.ts bleibt unberuehrt. role ist die Aufzaehlung Role: schema.prisma deklariert die Spalte so, die SQL-Funktion auth_lookup_user_by_username gibt sie als "Role" zurueck. Die Handannotation role: string in AuthLookupUserByUsernameRow war eine zweite Fassung desselben Wertes und faellt damit weg. SessionUser und UploadedPng in bug-reports.service.ts sind jetzt Pick<> der neuen Typen statt eigener Beschreibungen. Fixtures in auth.controller.spec.ts ergaenzt: sie uebergaben einen Aufrufer ohne username und ohne mustChangePassword - eine Form, die JwtStrategy nie erzeugt. Testzahlen unveraendert. noExplicitAny in apps/api/src: 149 -> 137. type-check 4/4, lint 5/5, apps/api 72/1143, apps/web 73/531. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
180 lines
5.8 KiB
TypeScript
180 lines
5.8 KiB
TypeScript
import {
|
|
BadRequestException,
|
|
Body,
|
|
Controller,
|
|
Get,
|
|
HttpCode,
|
|
Param,
|
|
Post,
|
|
Req,
|
|
Res,
|
|
UseGuards,
|
|
} from '@nestjs/common';
|
|
import { AuthGuard } from '@nestjs/passport';
|
|
import { Role } from '@prisma/client';
|
|
import { 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';
|
|
import { Roles } from './decorators/roles.decorator';
|
|
import { AdminResetPasswordDto } from './dto/admin-reset-password.dto';
|
|
import { ChangePasswordDto } from './dto/change-password.dto';
|
|
import { RequestResetDto, ResetPasswordDto } from './dto/reset-password.dto';
|
|
import { RolesGuard } from './guards/roles.guard';
|
|
import type { AuthUser, LocalAuthenticatedRequest } from './types/auth-user';
|
|
|
|
@Controller('auth')
|
|
export class AuthController {
|
|
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: AuthUser, 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
|
|
* Validates credentials via Passport local strategy, then issues JWT cookie.
|
|
*/
|
|
@Public()
|
|
@UseGuards(AuthGuard('local'))
|
|
@Post('login')
|
|
@HttpCode(200)
|
|
async login(
|
|
@Req() req: LocalAuthenticatedRequest,
|
|
@Res({ passthrough: true }) res: Response,
|
|
) {
|
|
return this.authService.login(req.user, res);
|
|
}
|
|
|
|
/**
|
|
* POST /auth/logout
|
|
* Clears the session cookie.
|
|
*/
|
|
@Post('logout')
|
|
@HttpCode(200)
|
|
logout(@Res({ passthrough: true }) res: Response) {
|
|
this.authService.logout(res);
|
|
return { message: 'Logged out' };
|
|
}
|
|
|
|
/**
|
|
* 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: AuthUser) {
|
|
return this.authService.getMe(user.tenantId, user.id);
|
|
}
|
|
|
|
/**
|
|
* POST /auth/request-reset
|
|
* Request a password reset email (D-03 self-service).
|
|
* @Public() -- no authentication required.
|
|
* T-02-12: Always returns 200 regardless of email existence.
|
|
*/
|
|
@Public()
|
|
@Post('request-reset')
|
|
@HttpCode(200)
|
|
async requestReset(@Body() dto: RequestResetDto) {
|
|
await this.authService.requestPasswordReset(dto.email);
|
|
return { message: 'If an account with this email exists, a reset link has been sent.' };
|
|
}
|
|
|
|
/**
|
|
* POST /auth/reset-password
|
|
* Reset password using a valid token (D-03 self-service).
|
|
* @Public() -- no authentication required (uses token for verification).
|
|
*/
|
|
@Public()
|
|
@Post('reset-password')
|
|
@HttpCode(200)
|
|
async resetPassword(@Body() dto: ResetPasswordDto) {
|
|
await this.authService.resetPassword(dto.token, dto.newPassword);
|
|
return { message: 'Password has been reset successfully.' };
|
|
}
|
|
|
|
/**
|
|
* POST /auth/change-password
|
|
* Change password for the currently logged-in user.
|
|
* Requires authentication (not @Public).
|
|
*/
|
|
@Post('change-password')
|
|
@HttpCode(200)
|
|
async changePassword(
|
|
@CurrentUser() user: AuthUser,
|
|
@Body() dto: ChangePasswordDto,
|
|
@Res({ passthrough: true }) res: Response,
|
|
) {
|
|
await this.authService.changePassword(
|
|
user.tenantId,
|
|
user.id,
|
|
dto.currentPassword,
|
|
dto.newPassword,
|
|
res,
|
|
);
|
|
return { message: 'Password changed successfully.' };
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
@UseGuards(RolesGuard)
|
|
@HttpCode(200)
|
|
async adminResetPassword(
|
|
@Param('userId') userId: string,
|
|
@Body() dto: AdminResetPasswordDto,
|
|
@CurrentUser() currentUser: AuthUser,
|
|
) {
|
|
const tenantId = await this.resolveTargetTenantId(currentUser, userId);
|
|
await this.authService.adminResetPassword(
|
|
tenantId,
|
|
currentUser.role,
|
|
userId,
|
|
dto.newPassword,
|
|
dto.mustChangePassword ?? true,
|
|
);
|
|
return { message: 'User password has been reset.' };
|
|
}
|
|
}
|