docs(07-03): complete DKV export + SMTP settings plan
- 07-03-SUMMARY.md: all 4 tasks documented, threat mitigations verified, user-files/ path resolution and MailModule factory priority chain explained - STATE.md: advanced to Plan 4 of 6, added 5 new decisions, metrics row - ROADMAP.md: 07-03 checked complete, Phase 7 progress 3/6
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
---
|
||||
phase: 07-dkv-fleet-module
|
||||
plan: 03
|
||||
subsystem: dkv-export-smtp-settings
|
||||
tags: [dkv, smtp, settings, nodemailer, xlsx, sheetjs, encryption, aes-256-gcm, mail-module, d-06]
|
||||
dependency_graph:
|
||||
requires: [CalendarModule (CalendarCryptoService), PrismaModule (global), DkvModule (Plan 01 types)]
|
||||
provides: [SettingsModule, SettingsService, DkvExportService, DkvMailService, migrated MailModule]
|
||||
affects:
|
||||
- apps/api/src/settings/settings.module.ts
|
||||
- apps/api/src/settings/settings.controller.ts
|
||||
- apps/api/src/settings/settings.service.ts
|
||||
- apps/api/src/settings/dto/smtp-config.dto.ts
|
||||
- apps/api/src/dkv/dkv-export.service.ts
|
||||
- apps/api/src/dkv/dkv-mail.service.ts
|
||||
- apps/api/src/mail/mail.module.ts
|
||||
- apps/api/src/app.module.ts
|
||||
tech_stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "SettingsService.getSmtpConfig: SMTP_SAFE_SELECT excludes encryptedPassword; GET returns hasPassword boolean (T-07-07)"
|
||||
- "SettingsService.saveSmtpConfig: CalendarCryptoService.encrypt() for AES-256-GCM password; empty password preserves existing (T-07-08)"
|
||||
- "SettingsService.testSmtpConfig: nodemailer.createTransport() + transport.verify(); returns only boolean (T-07-16)"
|
||||
- "SettingsService.getStartupSmtpConfig: findFirst() for tenant-agnostic MailModule factory (D-06); never logs decrypted password (T-07-11)"
|
||||
- "DkvExportService.buildExcelBuffer: XLSX.utils.aoa_to_sheet with 5-column header; Lieferdatum written as string (anti-pattern avoidance)"
|
||||
- "DkvExportService.writeAndPrune: server-side filename DKV_YYYY-MM_<nr>.xlsx (T-07-09); prune by mtime ascending; keep last 10 (D-15)"
|
||||
- "DkvMailService.sendExportEmail: createTransport() at send time from DB (Pitfall 3); error rethrows for D-16 retry (does not swallow)"
|
||||
- "MailModule.forRootAsync async factory: DB SmtpConfig priority 1; MAIL_HOST/PORT/USER/PASS priority 2; TESSERA_SMTP_* priority 3; localhost:1025 final fallback"
|
||||
key_files:
|
||||
created:
|
||||
- apps/api/src/settings/dto/smtp-config.dto.ts
|
||||
- apps/api/src/settings/settings.service.ts
|
||||
- apps/api/src/settings/settings.controller.ts
|
||||
- apps/api/src/settings/settings.module.ts
|
||||
- apps/api/src/dkv/dkv-export.service.ts
|
||||
- apps/api/src/dkv/dkv-mail.service.ts
|
||||
modified:
|
||||
- apps/api/src/mail/mail.module.ts
|
||||
- apps/api/src/app.module.ts
|
||||
decisions:
|
||||
- "SettingsService exports getStartupSmtpConfig (findFirst) for MailModule — single-tenant deployments use the one SmtpConfig row as the system mail transport"
|
||||
- "user-files/ path resolved via path.resolve(__dirname, '..', '..', '..', '..', 'user-files') from apps/api/dist/ — reaches monorepo root regardless of CWD"
|
||||
- "Prune ordering: mtime ascending (oldest mtime first), slice to delete oldest entries beyond 10-file limit"
|
||||
- "MailModule forRootAsync factory chain: DB SmtpConfig → MAIL_* env → TESSERA_SMTP_* env → localhost:1025 hardcoded (backward compat preserved)"
|
||||
- "DkvMailService injects SettingsService (not PrismaService + CalendarCryptoService directly) — avoids duplicate decryption logic"
|
||||
- "SettingsModule imports CalendarModule (not re-provides CalendarCryptoService) — single source of truth for AES key lifecycle"
|
||||
metrics:
|
||||
duration: 5min
|
||||
completed: "2026-06-27T00:15:00Z"
|
||||
tasks: 4
|
||||
files_created: 6
|
||||
files_modified: 2
|
||||
---
|
||||
|
||||
# Phase 07 Plan 03: DKV Export + SMTP Settings — Summary
|
||||
|
||||
SettingsModule (SMTP config backend), DkvExportService (xlsx generation + 10-file prune), DkvMailService (runtime nodemailer transport per send), and migrated MailModule (DB SmtpConfig priority with env fallback — D-06).
|
||||
|
||||
## Tasks Completed
|
||||
|
||||
| Task | Name | Commit | Key Files |
|
||||
|------|------|--------|-----------|
|
||||
| 1 | SettingsModule — SMTP config backend + connection test (DKV-05) | 1bec0e7 | settings.module.ts, settings.controller.ts, settings.service.ts, dto/smtp-config.dto.ts, app.module.ts |
|
||||
| 2 | DkvExportService — xlsx generation + user-files/ prune (DKV-04) | 6b76ca9 | dkv-export.service.ts |
|
||||
| 3 | DkvMailService — runtime SMTP transport with attachment (DKV-04) | 4deefb5 | dkv-mail.service.ts |
|
||||
| 4 | Migrate MailModule to DB-sourced SMTP transport with env fallback (D-06) | de48e35 | mail.module.ts |
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
### user-files/ Path Resolution
|
||||
|
||||
`DkvExportService` resolves the path using:
|
||||
```
|
||||
path.resolve(__dirname, '..', '..', '..', '..', 'user-files')
|
||||
```
|
||||
`__dirname` at runtime is `apps/api/dist/dkv/`. Going up 4 levels: `dkv/ → dist/ → api/ → apps/ → <repo-root>/`. This reaches `user-files/` at the monorepo root regardless of the process working directory. The path is never derived from request input (T-07-09).
|
||||
|
||||
### Export File Prune Ordering
|
||||
|
||||
Files matching `DKV_*.xlsx` in `user-files/` are collected, sorted by `mtime` ascending (oldest modification time first), and all entries beyond index 9 (i.e., beyond the last 10) are deleted via `fs.unlinkSync`. The `writeAndPrune` method handles write + prune atomically within a single synchronous call — the caller's processing lock (Plan 04) prevents interleaved operations (Pitfall 7).
|
||||
|
||||
### MailModule Factory Transport Resolution (D-06)
|
||||
|
||||
The `MailerModule.forRootAsync` factory is async and resolves the transport in this priority order:
|
||||
|
||||
1. **DB SmtpConfig** (`settingsService.getStartupSmtpConfig()` → `prisma.smtpConfig.findFirst()`): used when any row exists. In single-tenant deployments, the one SmtpConfig row serves as the system mail transport. Decrypted password is used only to build the transport object — never logged (T-07-11).
|
||||
2. **Env vars `MAIL_HOST` / `MAIL_PORT` / `MAIL_USER` / `MAIL_PASS`**: used when no DB row exists.
|
||||
3. **Legacy env vars `TESSERA_SMTP_HOST` / `TESSERA_SMTP_PORT` / `TESSERA_SMTP_USER` / `TESSERA_SMTP_PASSWORD`**: secondary fallback preserving backward compatibility.
|
||||
4. **Hardcoded `localhost:1025`**: final fallback for local dev (Mailhog).
|
||||
|
||||
No circular import exists: `MailModule → SettingsModule → CalendarModule` — no reverse edges.
|
||||
|
||||
### DkvMailService Transport Strategy
|
||||
|
||||
Transport is created via `nodemailer.createTransport()` inside `sendExportEmail()`, not at module init. This means:
|
||||
- SMTP config changes in the admin UI take effect on the next send without a service restart.
|
||||
- Contrast with `MailerModule`/`MailService` which configure transport once at startup.
|
||||
- This is the mandatory pattern for DKV mail per Research Pitfall 3.
|
||||
|
||||
## Verification Results
|
||||
|
||||
- `pnpm --filter @tessera/api type-check` exits 0: PASS
|
||||
- `SettingsModule` in `app.module.ts` imports: PASS
|
||||
- `SettingsModule` exports `SettingsService`: PASS
|
||||
- All controller handlers carry `@Roles(Role.ADMIN, Role.SUPER_ADMIN)`: PASS
|
||||
- `getSmtpConfig` present in settings.service.ts: PASS
|
||||
- `testSmtpConfig` present in settings.service.ts: PASS
|
||||
- `getStartupSmtpConfig` present in settings.service.ts: PASS
|
||||
- `getStartupSmtpConfig` called in mail.module.ts: PASS
|
||||
- `forRootAsync` in mail.module.ts: PASS
|
||||
- `MAIL_HOST` env fallback in mail.module.ts: PASS
|
||||
- `SettingsModule` imported in mail.module.ts: PASS
|
||||
- `aoa_to_sheet` in dkv-export.service.ts: PASS
|
||||
- `Lieferdatum` header in dkv-export.service.ts: PASS
|
||||
- 5-column header order exactly matching D-13: PASS
|
||||
- `MAX_EXPORT_FILES = 10` prune limit: PASS
|
||||
- Lieferdatum written as string (no Date wrapping in code): PASS
|
||||
- `createTransport` in dkv-mail.service.ts: PASS
|
||||
- No `MailerService` import in dkv-mail.service.ts: PASS
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 1 - Bug] "MailerService" string in Task 3 grep verification**
|
||||
- **Found during:** Task 3 verification
|
||||
- **Issue:** The plan's automated check `! grep -q "MailerService" apps/api/src/dkv/dkv-mail.service.ts` failed because a JSDoc comment contained the string "This service does NOT import or use MailerService from @nestjs-modules/mailer." The grep test is literal string matching and does not distinguish comments from code.
|
||||
- **Fix:** Replaced the comment wording to "This service uses nodemailer directly — NOT the @nestjs-modules/mailer abstraction." — no MailerService string in the file.
|
||||
- **Files modified:** apps/api/src/dkv/dkv-mail.service.ts
|
||||
- **Commit:** 4deefb5
|
||||
|
||||
## Known Stubs
|
||||
|
||||
None — all functionality is fully implemented. DkvExportService, DkvMailService, and SettingsService contain no placeholder values, hardcoded empty returns, or TODO items that would block Plan 04 orchestration.
|
||||
|
||||
## Threat Flags
|
||||
|
||||
None. All STRIDE threats in this plan's threat register were mitigated:
|
||||
- T-07-07: `SMTP_SAFE_SELECT` excludes `encryptedPassword`; GET returns `hasPassword` boolean; `encryptedPassword` stripped in controller before returning
|
||||
- T-07-08: All three endpoints (GET, PUT, POST test) carry `@Roles(Role.ADMIN, Role.SUPER_ADMIN)`; `SmtpConfigDto` validates host/port/encryption
|
||||
- T-07-09: `writeAndPrune` filename built from `invoiceMonth` + `rechnungsnummer` parameters (from parsed PDF), never from HTTP request input
|
||||
- T-07-10: `DkvMailService` catch block logs only `(error as Error).message` — no credentials, host, transport details
|
||||
- T-07-11: `getStartupSmtpConfig` decrypts password only to build the transport return object; no logging of the value
|
||||
- T-07-16: `testSmtpConfig` returns `{ success: boolean }` only; verify() failures logged with generic message (no credentials)
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
Files verified present:
|
||||
- apps/api/src/settings/dto/smtp-config.dto.ts ✓
|
||||
- apps/api/src/settings/settings.service.ts ✓
|
||||
- apps/api/src/settings/settings.controller.ts ✓
|
||||
- apps/api/src/settings/settings.module.ts ✓
|
||||
- apps/api/src/dkv/dkv-export.service.ts ✓
|
||||
- apps/api/src/dkv/dkv-mail.service.ts ✓
|
||||
- apps/api/src/mail/mail.module.ts (modified) ✓
|
||||
- apps/api/src/app.module.ts (modified) ✓
|
||||
|
||||
Commits verified in git log:
|
||||
- 1bec0e7 ✓ (feat(07-03): SettingsModule — SMTP config backend + connection test)
|
||||
- 6b76ca9 ✓ (feat(07-03): DkvExportService — xlsx generation + user-files/ prune)
|
||||
- 4deefb5 ✓ (feat(07-03): DkvMailService — runtime nodemailer transport with xlsx attachment)
|
||||
- de48e35 ✓ (feat(07-03): Migrate MailModule to DB-sourced SMTP transport with env fallback)
|
||||
Reference in New Issue
Block a user