--- phase: 02-authentication-multi-tenancy plan: 03 subsystem: auth, mail tags: [password-reset, smtp, nodemailer, mailer, force-change, interceptor, mailhog, i18n] # Dependency graph requires: - phase: 02-authentication-multi-tenancy/01 provides: "Backend auth (AuthModule, AuthService, JWT guards, Prisma schema with PasswordResetToken model)" - phase: 02-authentication-multi-tenancy/02 provides: "Frontend auth (login page, middleware, auth-actions, auth store, i18n keys)" provides: - "MailModule with SMTP transport configurable via ENV variables" - "Password reset self-service flow: request-reset + token-based reset (D-03)" - "Admin password reset endpoint for ADMIN/SUPER_ADMIN (D-03)" - "Change password endpoint for logged-in users" - "ForcePasswordChangeInterceptor blocking all routes when mustChangePassword=true (D-06)" - "Reset password request page at /reset-password" - "Token-based reset page at /reset-password/[token]" - "Change password page at /change-password (portal route group)" - "MailHog service in docker-compose.dev.yml for dev email testing" - "Forgot password link on login page" - "Complete DE/EN i18n for password reset and change flows" affects: [ldap-sync, user-management, admin-dashboard] # Tech tracking tech-stack: added: ["@nestjs-modules/mailer", "nodemailer", "ldapts", "@types/nodemailer"] patterns: - "MailModule with forRootAsync using ConfigService for SMTP ENV vars" - "Global NestJS interceptor for force-password-change enforcement (APP_INTERCEPTOR)" - "Fire-and-forget email sending with error logging (no throw on send failure)" - "Single-use password reset tokens with 1-hour expiry via PasswordResetToken model" - "Always-200 response on password reset request to prevent email enumeration" key-files: created: - "apps/api/src/mail/mail.module.ts" - "apps/api/src/mail/mail.service.ts" - "apps/api/src/auth/dto/reset-password.dto.ts" - "apps/api/src/auth/dto/change-password.dto.ts" - "apps/api/src/auth/dto/admin-reset-password.dto.ts" - "apps/api/src/auth/interceptors/force-password-change.interceptor.ts" - "apps/web/src/app/(auth)/reset-password/page.tsx" - "apps/web/src/app/(auth)/reset-password/[token]/page.tsx" - "apps/web/src/app/(portal)/change-password/page.tsx" modified: - "apps/api/src/auth/auth.service.ts" - "apps/api/src/auth/auth.controller.ts" - "apps/api/src/auth/auth.module.ts" - "apps/api/src/app.module.ts" - "apps/api/package.json" - "apps/web/src/app/(auth)/login/page.tsx" - "apps/web/src/messages/de.json" - "apps/web/src/messages/en.json" - "docker-compose.yml" - "docker-compose.dev.yml" key-decisions: - "Plain text emails for MVP -- no HTML templates needed for password reset emails" - "MailService swallows send failures with logging -- caller always returns 200 for enumeration prevention" - "ForcePasswordChangeInterceptor allows /auth/me in addition to /auth/change-password and /auth/logout so frontend can detect the flag" - "Admin reset defaults mustChangePassword to true -- admin-reset users are forced to pick their own password" - "ldapts installed alongside mailer packages as approved SUS package (needed for future LDAP plan)" patterns-established: - "MailModule: SMTP config via TESSERA_SMTP_* ENV vars, MailHog for dev" - "Global interceptor pattern: APP_INTERCEPTOR for cross-cutting request enforcement" - "Password reset tokens: UUID-based, single-use, 1-hour expiry, stored in DB" - "Email enumeration prevention: always return 200 on reset request regardless of email existence" requirements-completed: [AUTH-03, AUTH-04] # Metrics duration: 5min completed: 2026-06-18 --- # Phase 2 Plan 03: Password Reset & Mail Summary **Self-service password reset via email with single-use tokens, admin manual reset, force-password-change interceptor (D-06), and MailModule with MailHog for dev testing** ## Performance - **Duration:** 5 min - **Started:** 2026-06-18T11:43:25Z - **Completed:** 2026-06-18T11:48:42Z - **Tasks:** 2 (1 checkpoint:human-verify + 1 auto) - **Files modified:** 20 ## Accomplishments - MailModule with SMTP transport configurable via ENV variables, defaulting to MailHog for development - Complete password reset flow: public request-reset endpoint (always 200, no enumeration), token-based reset with 1-hour expiry and single-use enforcement - Admin password reset endpoint protected by ADMIN/SUPER_ADMIN roles (D-03) - Change password for logged-in users with current password verification - ForcePasswordChangeInterceptor as global APP_INTERCEPTOR blocks all API routes (except change-password, logout, me) when mustChangePassword=true, enforcing D-06 at the API level - Frontend pages: reset-password request form, token-based reset form with 3-second redirect on success, change-password form with force-change notice - MailHog added to docker-compose.dev.yml (SMTP on port 1025, web UI on port 8025) - "Forgot password?" link added to login page - Complete DE/EN i18n coverage for all new password flows ## Task Commits Each task was committed atomically: 1. **Task 1: Verify SUS-flagged packages** - (checkpoint:human-verify, pre-approved by user) 2. **Task 2: MailModule, password reset flow, force-change interceptor, frontend pages** - `ac617f4` (feat) ## Files Created/Modified ### Created - `apps/api/src/mail/mail.module.ts` - NestJS MailerModule with SMTP transport from ENV - `apps/api/src/mail/mail.service.ts` - Email sending: password reset (i18n) and welcome emails - `apps/api/src/auth/dto/reset-password.dto.ts` - RequestResetDto and ResetPasswordDto with validation - `apps/api/src/auth/dto/change-password.dto.ts` - ChangePasswordDto with validation - `apps/api/src/auth/dto/admin-reset-password.dto.ts` - AdminResetPasswordDto with validation - `apps/api/src/auth/interceptors/force-password-change.interceptor.ts` - Global interceptor enforcing D-06 - `apps/web/src/app/(auth)/reset-password/page.tsx` - Password reset request form - `apps/web/src/app/(auth)/reset-password/[token]/page.tsx` - Token-based password reset form - `apps/web/src/app/(portal)/change-password/page.tsx` - Change password form (portal route group) ### Modified - `apps/api/src/auth/auth.service.ts` - Added requestPasswordReset, resetPassword, changePassword, adminResetPassword methods - `apps/api/src/auth/auth.controller.ts` - Added 4 new endpoints: request-reset, reset-password, change-password, admin-reset-password/:userId - `apps/api/src/auth/auth.module.ts` - Imported MailModule - `apps/api/src/app.module.ts` - Imported MailModule, registered ForcePasswordChangeInterceptor as APP_INTERCEPTOR - `apps/api/package.json` - Added @nestjs-modules/mailer, nodemailer, ldapts, @types/nodemailer - `apps/web/src/app/(auth)/login/page.tsx` - Added "Forgot password?" link - `apps/web/src/messages/de.json` - Added resetPassword and changePassword i18n keys - `apps/web/src/messages/en.json` - Added resetPassword and changePassword i18n keys - `docker-compose.yml` - Added TESSERA_SMTP_* and TESSERA_APP_URL env vars to api service - `docker-compose.dev.yml` - Added mailhog service and SMTP env vars for api ## Decisions Made - **Plain text emails for MVP:** No HTML email templates -- plain text emails with reset links are sufficient for MVP. HTML templates can be added later with handlebars adapter. - **Fire-and-forget email sending:** MailService catches and logs send failures without throwing. The caller (requestPasswordReset) always returns success, both for error resilience and email enumeration prevention. - **ForcePasswordChangeInterceptor allows /auth/me:** In addition to /auth/change-password and /auth/logout, the interceptor allows GET /auth/me so the frontend can detect the mustChangePassword flag and show appropriate UI. - **Admin reset defaults mustChangePassword to true:** When an admin resets a user's password, the user is forced to change it on next login. This is configurable via the DTO. - **mustChangePassword included in JWT payload:** Added to the login response and JWT token so the frontend middleware can detect it for client-side redirect. ## Deviations from Plan None - plan executed exactly as written. ## Threat Model Coverage | Threat ID | Status | Implementation | |-----------|--------|----------------| | T-02-12 | Mitigated | requestPasswordReset always returns 200; MailService swallows errors | | T-02-13 | Mitigated | UUID tokens, single-use (usedAt check), 1-hour expiry | | T-02-14 | Mitigated | ForcePasswordChangeInterceptor as global APP_INTERCEPTOR; allowlist of exempt paths | | T-02-15 | Mitigated | adminResetPassword endpoint requires ADMIN/SUPER_ADMIN role via @Roles decorator | | T-02-SC | Mitigated | SUS packages verified by human before installation (checkpoint:human-verify) | ## Issues Encountered None - all packages installed cleanly, type-check passed on first attempt. ## Known Stubs None - all features are fully wired with data sources. ## User Setup Required **SMTP configuration for production:** The development setup uses MailHog (no config needed). For production, set these environment variables: - `TESSERA_SMTP_HOST` - SMTP server hostname - `TESSERA_SMTP_PORT` - SMTP port (587 for STARTTLS, 465 for SSL) - `TESSERA_SMTP_USER` - SMTP authentication username - `TESSERA_SMTP_PASSWORD` - SMTP authentication password - `TESSERA_SMTP_FROM` - Sender email address - `TESSERA_APP_URL` - Application URL for reset links For development, MailHog is automatically available at http://localhost:8025 when using docker-compose.dev.yml. ## Next Phase Readiness - Password reset and force-change flows are complete -- auth lifecycle is now fully implemented - MailModule is reusable for future notification features - ldapts package is installed and ready for Plan 02-04 (LDAP sync) - Ready for Plan 02-05 (RLS + tenant management) or any subsequent plans ## Self-Check: PASSED All 9 created files verified on disk. Task commit (ac617f4) verified in git log. Type-check passes for all packages. --- *Phase: 02-authentication-multi-tenancy* *Completed: 2026-06-18*