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
@@ -0,0 +1,15 @@
import { IsBoolean, IsOptional, IsString, MinLength } from 'class-validator';
/**
* DTO for admin password reset.
* POST /auth/admin-reset-password/:userId
*/
export class AdminResetPasswordDto {
@IsString()
@MinLength(8)
newPassword!: string;
@IsBoolean()
@IsOptional()
mustChangePassword?: boolean;
}
@@ -0,0 +1,14 @@
import { IsString, MinLength } from 'class-validator';
/**
* DTO for changing the current user's password.
* POST /auth/change-password
*/
export class ChangePasswordDto {
@IsString()
currentPassword!: string;
@IsString()
@MinLength(8)
newPassword!: string;
}
@@ -0,0 +1,25 @@
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
/**
* DTO for requesting a password reset email.
* POST /auth/request-reset
*/
export class RequestResetDto {
@IsEmail()
@IsNotEmpty()
email!: string;
}
/**
* DTO for resetting a password with a token.
* POST /auth/reset-password
*/
export class ResetPasswordDto {
@IsString()
@IsNotEmpty()
token!: string;
@IsString()
@MinLength(8)
newPassword!: string;
}