docs(02-04): LDAP integration complete
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
---
|
||||
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*
|
||||
@@ -0,0 +1,35 @@
|
||||
# Plan 02-04: LDAP Integration — Summary
|
||||
|
||||
**Status:** Complete
|
||||
**Date:** 2026-06-19
|
||||
|
||||
## What Was Built
|
||||
|
||||
### Task 1: Backend LDAP Module
|
||||
- LdapService using ldapts library for directory sync (NOT as auth — anti-pattern avoidance)
|
||||
- LdapConfigService for per-tenant LDAP configuration (D-18)
|
||||
- LdapSyncScheduler with configurable auto-sync interval (D-14)
|
||||
- Field mapping: configurable with defaults (displayName→Name, mail→Email, sAMAccountName→Username) (D-16)
|
||||
- Custom field mapping support (D-17)
|
||||
- Deactivation of removed LDAP users (D-15)
|
||||
- Manual sync endpoint POST /ldap/sync
|
||||
- Prisma schema extended with LdapConfig and LdapFieldMapping models
|
||||
|
||||
### Task 2: Frontend LDAP Admin UI
|
||||
- LDAP settings page at /admin/ldap
|
||||
- Connection configuration form (server URL, base DN, bind user, filter)
|
||||
- Field mapping editor with default + custom fields
|
||||
- Test connection button
|
||||
- Manual sync trigger button
|
||||
- Sidebar navigation updated with LDAP admin link
|
||||
- i18n keys for DE/EN
|
||||
|
||||
## Commits
|
||||
- `f928cd7`: LdapModule with sync service, config service, scheduler, controller
|
||||
- `6e19591`: LDAP admin UI with config, mapping editor, sync trigger
|
||||
|
||||
## Requirements Addressed
|
||||
- AUTH-06: Users can be imported from LDAP/AD ✓
|
||||
- TNNT-01: LDAP data tenant-isolated via RLS ✓
|
||||
- TNNT-02: LDAP config per tenant ✓
|
||||
- TNNT-03: Per-request tenant context in LDAP operations ✓
|
||||
Reference in New Issue
Block a user