Files
tessera-ctl/.planning/phases/02-authentication-multi-tenancy/02-03-SUMMARY.md
T

9.9 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established requirements-completed duration completed
02-authentication-multi-tenancy 03 auth, mail
password-reset
smtp
nodemailer
mailer
force-change
interceptor
mailhog
i18n
phase provides
02-authentication-multi-tenancy/01 Backend auth (AuthModule, AuthService, JWT guards, Prisma schema with PasswordResetToken model)
phase provides
02-authentication-multi-tenancy/02 Frontend auth (login page, middleware, auth-actions, auth store, i18n keys)
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
ldap-sync
user-management
admin-dashboard
added patterns
@nestjs-modules/mailer
nodemailer
ldapts
@types/nodemailer
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
created modified
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
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
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)
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
AUTH-03
AUTH-04
5min 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