feat(02-03): password reset flow, force-change interceptor, MailModule

- MailModule with SMTP transport configured from ENV variables
- MailService for password reset and welcome emails (plain text, i18n)
- Password reset flow: request-reset (public), reset-password (token-based)
- Change password for logged-in users with current password verification
- Admin reset password endpoint (ADMIN/SUPER_ADMIN only, D-03)
- ForcePasswordChangeInterceptor blocks all routes except change-password,
  logout, me when mustChangePassword=true (D-06, Pitfall 5)
- Frontend: reset-password request page, token reset page, change-password page
- Forgot password link added to login page
- MailHog service added to docker-compose.dev.yml for dev email testing
- SMTP env vars added to docker-compose.yml (defaults to MailHog)
- Complete DE/EN i18n coverage for reset and change password flows
- SUS packages installed: @nestjs-modules/mailer, nodemailer, ldapts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 13:48:23 +02:00
parent eaaa9adfa5
commit ac617f4fe5
20 changed files with 4216 additions and 12 deletions
+75
View File
@@ -1,17 +1,25 @@
import {
Body,
Controller,
Get,
HttpCode,
Param,
Post,
Req,
Res,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Role } from '@prisma/client';
import { Request, Response } from 'express';
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';
@Controller('auth')
export class AuthController {
@@ -51,4 +59,71 @@ export class AuthController {
me(@CurrentUser() user: any) {
return user;
}
/**
* 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: any,
@Body() dto: ChangePasswordDto,
) {
await this.authService.changePassword(
user.sub,
dto.currentPassword,
dto.newPassword,
);
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.
*/
@Post('admin-reset-password/:userId')
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
@UseGuards(RolesGuard)
@HttpCode(200)
async adminResetPassword(
@Param('userId') userId: string,
@Body() dto: AdminResetPasswordDto,
) {
await this.authService.adminResetPassword(
userId,
dto.newPassword,
dto.mustChangePassword ?? true,
);
return { message: 'User password has been reset.' };
}
}