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 { 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.' }; } }