de06794e67
6 plans covering full pipeline: PDF parsing foundation (Wave 0), inbox providers + export/SMTP services (Wave 1), pipeline orchestration + frontend pages + settings UI (Wave 2). Includes D-06 MailModule DB-config migration and Nyquist validation strategy. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
284 lines
22 KiB
Markdown
284 lines
22 KiB
Markdown
---
|
|
phase: 07-dkv-fleet-module
|
|
plan: 03
|
|
type: execute
|
|
wave: 1
|
|
depends_on: ["07-01"]
|
|
files_modified:
|
|
- 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-mail.service.ts
|
|
- apps/api/src/dkv/dkv-export.service.ts
|
|
- apps/api/src/mail/mail.module.ts
|
|
- apps/api/src/mail/mail.service.ts
|
|
- apps/api/src/app.module.ts
|
|
autonomous: true
|
|
requirements: [DKV-04, DKV-05]
|
|
user_setup: []
|
|
|
|
must_haves:
|
|
truths:
|
|
- "SMTP config (host, port, encryption, username, encrypted password, from address) can be saved and read per tenant"
|
|
- "GET /settings/smtp never returns the decrypted or encrypted password to the client"
|
|
- "An SMTP connection can be verified via POST /settings/smtp/test"
|
|
- "An xlsx buffer with exactly 5 columns can be generated from export rows"
|
|
- "Export files are written to user-files/ and pruned to the last 10"
|
|
- "An email with an xlsx attachment can be sent via a runtime nodemailer transport built from DB SMTP config"
|
|
- "The existing MailModule reads its SMTP transport from the DB SmtpConfig (priority 1) and falls back to env vars when no DB config exists (D-06)"
|
|
artifacts:
|
|
- path: "apps/api/src/settings/settings.service.ts"
|
|
provides: "SmtpConfig CRUD + encryption/decryption + test + startup transport accessor"
|
|
contains: "getSmtpConfig"
|
|
- path: "apps/api/src/dkv/dkv-export.service.ts"
|
|
provides: "buildExcelBuffer + resolveFahrzeug + 10-file prune"
|
|
min_lines: 40
|
|
- path: "apps/api/src/dkv/dkv-mail.service.ts"
|
|
provides: "dynamic nodemailer transport send with attachment"
|
|
contains: "createTransport"
|
|
- path: "apps/api/src/mail/mail.module.ts"
|
|
provides: "MailerModule.forRootAsync factory sourcing SMTP from DB SmtpConfig with env fallback (D-06)"
|
|
contains: "forRootAsync"
|
|
key_links:
|
|
- from: "apps/api/src/dkv/dkv-mail.service.ts"
|
|
to: "nodemailer.createTransport"
|
|
via: "transport built per-send from SmtpConfig (NOT @nestjs-modules/mailer)"
|
|
pattern: "createTransport"
|
|
- from: "apps/api/src/settings/settings.service.ts"
|
|
to: "CalendarCryptoService"
|
|
via: "encrypt/decrypt SMTP password"
|
|
pattern: "crypto.(encrypt|decrypt)"
|
|
- from: "apps/api/src/mail/mail.module.ts"
|
|
to: "SettingsService"
|
|
via: "MailerModule.forRootAsync factory injects SettingsService to read DB SMTP config (D-06)"
|
|
pattern: "forRootAsync"
|
|
- from: "apps/api/src/app.module.ts"
|
|
to: "SettingsModule"
|
|
via: "imports array registration"
|
|
pattern: "SettingsModule"
|
|
---
|
|
|
|
<objective>
|
|
Build the export-and-deliver half of the pipeline plus shared SMTP configuration: the SettingsModule (SmtpConfig CRUD with encryption + connection test, ADMIN-only), the DkvExportService (xlsx generation + user-files/ management with 10-file prune), and the DkvMailService (runtime nodemailer transport that reads SMTP config from DB so config changes take effect immediately). Register SettingsModule in AppModule. Finally, migrate the existing MailModule (`apps/api/src/mail/`) so its transport is sourced from the DB SmtpConfig with an env-var fallback — this satisfies D-06 ("existing mail module must read SMTP config from DB instead of hardcoded env vars").
|
|
|
|
Purpose: Delivers DKV-04 (Excel + SMTP send) backend and DKV-05 (SMTP settings storage), and closes D-06 by removing the hardcoded env-only SMTP transport from the existing MailModule. The runtime-transport approach for DkvMailService is mandatory because @nestjs-modules/mailer cannot change its transport after startup (Research Pitfall 3); the existing MailModule keeps @nestjs-modules/mailer but builds its startup transport from the DB default SmtpConfig.
|
|
Output: SettingsModule (SMTP backend + test), DkvExportService, DkvMailService, migrated MailModule.
|
|
</objective>
|
|
|
|
## Phase Goal
|
|
|
|
**Als** Administrator **möchte ich** DKV-Tankkarten-Rechnungen automatisch aus einem E-Mail-Postfach verarbeiten lassen, **damit** Flotten-Tankdaten ohne manuelle Eingabe als Excel-Datei exportiert und per SMTP zugestellt werden.
|
|
|
|
This plan delivers the export + delivery + SMTP-config capability — the exit point of the pipeline.
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
|
@$HOME/.claude/gsd-core/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/phases/07-dkv-fleet-module/07-CONTEXT.md
|
|
@.planning/phases/07-dkv-fleet-module/07-RESEARCH.md
|
|
@.planning/phases/07-dkv-fleet-module/07-PATTERNS.md
|
|
@apps/api/src/dkv/dkv.types.ts
|
|
@apps/api/src/calendar/crypto.service.ts
|
|
@apps/api/src/calendar/calendar.service.ts
|
|
@apps/api/src/ldap/ldap.controller.ts
|
|
@apps/api/src/mail/mail.service.ts
|
|
@apps/api/src/mail/mail.module.ts
|
|
</context>
|
|
|
|
## Artifacts this phase produces (Plan 03 portion)
|
|
|
|
New symbols (exclude from drift verification):
|
|
- Classes `SettingsModule`, `SettingsController`, `SettingsService`
|
|
- DTO `SmtpConfigDto`
|
|
- Class `DkvExportService` (`buildExcelBuffer`, `resolveFahrzeug`, `writeAndPrune`)
|
|
- Class `DkvMailService` (`sendExportEmail`)
|
|
- `SettingsModule` registered in `apps/api/src/app.module.ts`
|
|
- New `SettingsService` accessors `testSmtpConfig()` and `getStartupSmtpConfig()` (tenant-agnostic, used by the MailModule factory)
|
|
|
|
Modified (existing) symbols:
|
|
- `apps/api/src/mail/mail.module.ts` — `MailerModule.forRootAsync` factory rewired to source the transport from the DB default SmtpConfig with env fallback (D-06)
|
|
- `apps/api/src/mail/mail.service.ts` — unchanged logic; verified to still compile against the rewired MailerModule
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: SettingsModule — SMTP config backend + connection test (DKV-05)</name>
|
|
<files>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/app.module.ts</files>
|
|
<read_first>
|
|
- apps/api/src/ldap/ldap.controller.ts — controller shell, `@Roles(Role.ADMIN, Role.SUPER_ADMIN)`, tenant extraction `req.tenantId` + BadRequestException
|
|
- apps/api/src/calendar/calendar.service.ts lines 50-67 — SAFE_SELECT pattern excluding encrypted fields
|
|
- apps/api/src/calendar/crypto.service.ts — encrypt/decrypt API (CalendarCryptoService, exported from CalendarModule in Plan 01)
|
|
- apps/api/src/calendar/calendar.module.ts — module wiring reference (how CalendarCryptoService is provided/imported)
|
|
- apps/api/src/app.module.ts — imports array (add SettingsModule; ScheduleModule already added in Plan 01)
|
|
- 07-PATTERNS.md "settings.controller.ts" — GET/PUT /settings/smtp routes, hasPassword boolean response
|
|
- 07-RESEARCH.md Prisma SmtpConfig model field names + "Pattern 6: Dynamic SMTP via Nodemailer" (transport build / verify)
|
|
</read_first>
|
|
<action>
|
|
Create the SettingsModule (controller + service + dto). Import CalendarModule (or provide CalendarCryptoService) so `CalendarCryptoService` can be injected; PrismaModule is global. The module MUST list `SettingsService` in its `exports` array (Task 4 wires it into MailModule).
|
|
`SmtpConfigDto`: host `@IsString() @IsNotEmpty()`; port `@IsInt() @Min(1) @Max(65535)`; encryption `@IsIn(['none','starttls','ssl-tls'])`; username optional string; password optional string; fromAddress `@IsEmail()`.
|
|
`SettingsController` with `@Controller('settings')`, every handler `@Roles(Role.ADMIN, Role.SUPER_ADMIN)`, tenant extracted from `req.tenantId` (throw BadRequestException 'No tenant context' when absent):
|
|
- `GET /settings/smtp` → returns config with `encryptedPassword` stripped and a `hasPassword` boolean added; returns null when none.
|
|
- `PUT /settings/smtp` → upsert via service.
|
|
- `POST /settings/smtp/test` (body via `SmtpConfigDto`) → calls `service.testSmtpConfig(tenantId, dto)` and returns `{ success: boolean }`. This backs the UI-SPEC Surface C "Verbindung testen" button consumed by Plan 07-06.
|
|
`SettingsService`: `getSmtpConfig(tenantId)` using a `SMTP_SAFE_SELECT` that excludes `encryptedPassword`; `saveSmtpConfig(tenantId, dto)` that encrypts `dto.password` with `crypto.encrypt(...)` only when a new password is provided (preserve existing on empty), upserts on `tenantId @unique`; `getDecryptedSmtpConfig(tenantId)` (internal, used by DkvMailService) returning host/port/encryption/username/fromAddress plus `decryptedPassword`; `testSmtpConfig(tenantId, dto)` that builds a `nodemailer.createTransport(...)` from the submitted dto (use `dto.password` when provided, else the stored decrypted password) and runs `transport.verify()`, returning a boolean (generic error on failure, never logging credentials — T-05-13). Never log decrypted values (T-05-13).
|
|
Register `SettingsModule` in apps/api/src/app.module.ts imports array.
|
|
</action>
|
|
<verify>
|
|
<automated>pnpm --filter @tessera/api type-check && grep -q "getSmtpConfig" apps/api/src/settings/settings.service.ts && grep -q "testSmtpConfig" apps/api/src/settings/settings.service.ts && grep -q "SettingsModule" apps/api/src/app.module.ts && grep -q "Roles(Role.ADMIN" apps/api/src/settings/settings.controller.ts && grep -q "exports" apps/api/src/settings/settings.module.ts</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `GET /settings/smtp` handler strips `encryptedPassword` and adds `hasPassword` boolean
|
|
- All controller handlers (GET, PUT, POST test) carry `@Roles(Role.ADMIN, Role.SUPER_ADMIN)` (V4 access control)
|
|
- `SettingsService.saveSmtpConfig` calls `crypto.encrypt` for the password
|
|
- `SettingsService` exposes an internal decrypt method returning `decryptedPassword`
|
|
- `SettingsService.testSmtpConfig` builds a transport and calls `transport.verify()`, returning a boolean; `POST /settings/smtp/test` returns `{ success: boolean }`
|
|
- `SettingsModule` exports `SettingsService` and appears in `apps/api/src/app.module.ts` imports
|
|
- `pnpm --filter @tessera/api type-check` exits 0
|
|
</acceptance_criteria>
|
|
<done>Per-tenant SMTP config can be stored encrypted, read back safely, and connection-tested; module registered and exports SettingsService.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: DkvExportService — xlsx generation + user-files/ prune (DKV-04)</name>
|
|
<files>apps/api/src/dkv/dkv-export.service.ts</files>
|
|
<read_first>
|
|
- 07-RESEARCH.md "Pattern 5: SheetJS Excel Generation" — aoa_to_sheet, book_new, XLSX.write buffer
|
|
- 07-RESEARCH.md "Fahrzeug Format String Resolution" — resolveFahrzeug template replacement
|
|
- 07-RESEARCH.md "Pitfall 7" — 10-file prune race condition; D-12 filename, D-13 columns, D-14 one file per invoice, D-15 keep last 10
|
|
- 07-PATTERNS.md "dkv-export.service.ts" — service shell, buildExcelBuffer, processing-lock note
|
|
- apps/api/src/dkv/dkv.types.ts — ExportRow, DkvVehicleMaster shape
|
|
</read_first>
|
|
<action>
|
|
Implement `@Injectable() DkvExportService` with `new Logger(...)`.
|
|
`resolveFahrzeug(vehicle, formatString)`: replace `{Marke}`,`{Modell}`,`{Kennzeichen}`,`{Fahrer}` tokens (default format `{Marke}/{Modell}/{Kennzeichen}` per D-19).
|
|
`buildExcelBuffer(rows: ExportRow[]): Buffer`: header row exactly `['Lieferdatum','Fahrzeug','Fahrer','Ort','Kilometerstand']` (D-13 order), write Lieferdatum as the already-formatted German date STRING (never a JS Date — Research anti-pattern), Kilometerstand as number; `XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' })`.
|
|
`writeAndPrune(buffer, rechnungsnummer, invoiceMonth): string`: compute filename `DKV_YYYY-MM_<Rechnungsnummer>.xlsx` (D-12) server-side only (never user-supplied — path-traversal mitigation), write into the `user-files/` directory (create if missing), then list DKV_*.xlsx files sorted by mtime and delete all but the newest 10 (D-15). Return the filename.
|
|
Guard against the prune race (Pitfall 7): expose the write+prune as a single method so the orchestrator (Plan 04) serializes processing; do not interleave count-check and write.
|
|
Resolve the user-files/ path relative to the repo/app root, not from request input.
|
|
</action>
|
|
<verify>
|
|
<automated>pnpm --filter @tessera/api type-check && grep -q "aoa_to_sheet" apps/api/src/dkv/dkv-export.service.ts && grep -q "Lieferdatum" apps/api/src/dkv/dkv-export.service.ts</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `buildExcelBuffer` produces a sheet whose first row is exactly the 5 headers in D-13 order
|
|
- Lieferdatum is written as a string (no `new Date(` wrapping of the Lieferdatum value)
|
|
- `writeAndPrune` builds the filename server-side as `DKV_YYYY-MM_<nr>.xlsx` and never from request input
|
|
- Prune logic keeps at most 10 `DKV_*.xlsx` files
|
|
- `resolveFahrzeug` replaces all four placeholder tokens
|
|
- `pnpm --filter @tessera/api type-check` exits 0
|
|
</acceptance_criteria>
|
|
<done>Excel buffers with the exact 5-column contract are generated and export files are pruned to the last 10.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 3: DkvMailService — runtime SMTP transport with attachment (DKV-04)</name>
|
|
<files>apps/api/src/dkv/dkv-mail.service.ts</files>
|
|
<read_first>
|
|
- 07-RESEARCH.md "Pattern 6: Dynamic SMTP via Nodemailer" — createTransport at send time, secure/requireTLS mapping
|
|
- 07-RESEARCH.md "Pitfall 3" — @nestjs-modules/mailer cannot change transport at runtime; use nodemailer directly
|
|
- 07-PATTERNS.md "dkv-mail.service.ts" — imports, error handling (log + rethrow for retry), do NOT use MailerService
|
|
- apps/api/src/mail/mail.service.ts — existing MailService (its logic is left unchanged in Task 3; this DkvMailService is a separate service)
|
|
</read_first>
|
|
<action>
|
|
Implement `@Injectable() DkvMailService` with `new Logger(...)`. Inject SettingsService (or PrismaService + CalendarCryptoService) to load decrypted SMTP config.
|
|
`sendExportEmail(tenantId, recipient, attachmentBuffer, filename)`: load decrypted SMTP config for the tenant; build `nodemailer.createTransport({ host, port, secure: encryption==='ssl-tls', requireTLS: encryption==='starttls', auth: username ? { user, pass: decryptedPassword } : undefined })`; `sendMail({ from: fromAddress, to: recipient, subject: 'DKV Flottenabrechnung: '+filename, text, attachments: [{ filename, content: attachmentBuffer, contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }] })`.
|
|
On failure: log a generic message (never credentials, T-05-13) and RETHROW so the orchestrator (Plan 04) can run the 3-retry exponential backoff per D-16. Do NOT swallow the error (unlike MailService).
|
|
Do NOT import or use `MailerService` from @nestjs-modules/mailer.
|
|
</action>
|
|
<verify>
|
|
<automated>pnpm --filter @tessera/api type-check && grep -q "createTransport" apps/api/src/dkv/dkv-mail.service.ts && ! grep -q "MailerService" apps/api/src/dkv/dkv-mail.service.ts</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- Source uses `nodemailer.createTransport(` built from DB config at send time
|
|
- Source does NOT reference `MailerService`
|
|
- Attachment contentType is the xlsx OOXML mime type
|
|
- Error path rethrows (does not swallow) so retries can run
|
|
- `pnpm --filter @tessera/api type-check` exits 0
|
|
</acceptance_criteria>
|
|
<done>DKV exports can be emailed with the xlsx attachment via a per-send transport that reflects current DB SMTP config.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 4: Migrate MailModule to DB-sourced SMTP transport with env fallback (D-06)</name>
|
|
<files>apps/api/src/mail/mail.module.ts, apps/api/src/mail/mail.service.ts, apps/api/src/settings/settings.service.ts</files>
|
|
<read_first>
|
|
- apps/api/src/mail/mail.module.ts — current `MailerModule.forRootAsync` factory reading TESSERA_SMTP_* env vars only (this is what D-06 forbids; it must read DB config first)
|
|
- apps/api/src/mail/mail.service.ts — consumes `MailerService`; verify it still compiles after the factory change (no logic change expected)
|
|
- apps/api/src/settings/settings.service.ts — add a tenant-agnostic startup accessor here (Task 1 created this file)
|
|
- apps/api/src/calendar/crypto.service.ts — decrypt API for the stored SMTP password
|
|
- 07-CONTEXT.md D-05 (SMTP lives in general settings) and D-06 (existing mail module must read SMTP from DB)
|
|
</read_first>
|
|
<action>
|
|
This task closes the D-06 violation. User decision: Option 2 — migrate the existing `mail.service.ts`/`mail.module.ts` to DB-config + env-fallback rather than building a parallel service.
|
|
|
|
1. In `apps/api/src/settings/settings.service.ts`, add a tenant-agnostic accessor `async getStartupSmtpConfig()`: `prisma.smtpConfig.findFirst()`; if a row exists, decrypt its password via `CalendarCryptoService` and return `{ host, port, secure: encryption==='ssl-tls', requireTLS: encryption==='starttls', username, password: decryptedPassword, fromAddress }`; return `null` when no row exists. Never log the decrypted password (T-05-13). This is the DB default transport used for system mail (e.g. password-reset) — document in the SUMMARY that single-tenant deployments use the first SmtpConfig row.
|
|
|
|
2. Rewrite `apps/api/src/mail/mail.module.ts` to keep `MailerModule.forRootAsync(...)` but change the factory to source the transport from the DB SmtpConfig (priority 1) with an env-var fallback (priority 2):
|
|
- `imports: [SettingsModule]` and `inject: [SettingsService, ConfigService]`.
|
|
- Factory body: `const db = await settingsService.getStartupSmtpConfig();` Build `transport`/`defaults` from `db` when non-null. When `db` is null, fall back to env vars per the checker contract: `MAIL_HOST`, `MAIL_PORT`, `MAIL_USER`, `MAIL_PASS` (read via `configService.get`). Preserve backward compatibility by chaining to the existing `TESSERA_SMTP_*` names and current defaults (`localhost:1025`) as the final fallback so the password-reset flow keeps working: e.g. `host = MAIL_HOST ?? TESSERA_SMTP_HOST ?? 'localhost'`, `port = MAIL_PORT ?? TESSERA_SMTP_PORT ?? 1025`, `user = MAIL_USER ?? TESSERA_SMTP_USER ?? ''`, `pass = MAIL_PASS ?? TESSERA_SMTP_PASSWORD ?? ''`. Map `secure`: from DB use `encryption==='ssl-tls'`; from env keep `TESSERA_SMTP_SECURE==='true'`. `defaults.from` from DB `fromAddress` else `TESSERA_SMTP_FROM` default.
|
|
- This is an async factory; `useFactory` must be `async`.
|
|
|
|
3. `apps/api/src/mail/mail.service.ts`: no behavioral change is required (it injects `MailerService`, which still exists). Confirm it compiles against the rewired module. Do not alter the password-reset / welcome-email logic.
|
|
|
|
Beware circular imports: the chain `AuthModule → MailModule → SettingsModule → CalendarModule` has no cycle; do NOT make SettingsModule import MailModule.
|
|
</action>
|
|
<verify>
|
|
<automated>pnpm --filter @tessera/api type-check && grep -q "forRootAsync" apps/api/src/mail/mail.module.ts && grep -q "getStartupSmtpConfig" apps/api/src/settings/settings.service.ts && grep -q "getStartupSmtpConfig" apps/api/src/mail/mail.module.ts && grep -Eq "MAIL_HOST" apps/api/src/mail/mail.module.ts && grep -q "SettingsModule" apps/api/src/mail/mail.module.ts</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `apps/api/src/mail/mail.module.ts` still calls `MailerModule.forRootAsync(`
|
|
- The factory injects `SettingsService` and calls `getStartupSmtpConfig()` (DB priority 1)
|
|
- When no DB SmtpConfig row exists, the factory falls back to env vars `MAIL_HOST`/`MAIL_PORT`/`MAIL_USER`/`MAIL_PASS` (with existing `TESSERA_SMTP_*` preserved as secondary fallback)
|
|
- `SettingsService.getStartupSmtpConfig()` decrypts the stored password and never logs it
|
|
- `apps/api/src/mail/mail.module.ts` imports `SettingsModule`
|
|
- `pnpm --filter @tessera/api type-check` exits 0
|
|
</acceptance_criteria>
|
|
<done>The existing MailModule sources its SMTP transport from the DB SmtpConfig with an env-var fallback — D-06 satisfied without a parallel mail service.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## Trust Boundaries
|
|
|
|
| Boundary | Description |
|
|
|----------|-------------|
|
|
| client → /settings/smtp | Admin supplies SMTP host/credentials |
|
|
| API → external SMTP server | Outbound mail with attachment + connection verify |
|
|
| API → user-files/ filesystem | Export file writes |
|
|
| DB SmtpConfig → MailModule factory | Stored encrypted SMTP password decrypted at startup |
|
|
|
|
## STRIDE Threat Register
|
|
|
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
|
|-----------|----------|-----------|-------------|-----------------|
|
|
| T-07-07 | Information Disclosure | settings.service/controller | mitigate | SMTP_SAFE_SELECT excludes encryptedPassword; GET returns hasPassword boolean only; password encrypted via CalendarCryptoService (V6) |
|
|
| T-07-08 | Tampering | settings.controller.ts | mitigate | All SMTP endpoints (GET/PUT/test) are `@Roles(Role.ADMIN, Role.SUPER_ADMIN)` only (V4); host/port/encryption validated via SmtpConfigDto (open-relay defense) |
|
|
| T-07-09 | Tampering | dkv-export.service.ts | mitigate | Export filename generated server-side (`DKV_YYYY-MM_<nr>.xlsx`), never from request — path-traversal prevented |
|
|
| T-07-10 | Information Disclosure | dkv-mail.service.ts | mitigate | Generic error logging; decrypted SMTP password never logged (T-05-13) |
|
|
| T-07-11 | Information Disclosure | mail.module.ts / settings.service.getStartupSmtpConfig | mitigate | Decrypted SMTP password used only to build the transport at startup; never logged (T-05-13); env fallback values are not echoed |
|
|
| T-07-16 | Information Disclosure | settings.service.testSmtpConfig | mitigate | Connection test returns only a boolean; verify() failures logged generically without credentials (T-05-13) |
|
|
</threat_model>
|
|
|
|
<verification>
|
|
- `pnpm --filter @tessera/api type-check` exits 0
|
|
- SettingsModule registered + exports SettingsService; SMTP CRUD never leaks password; test endpoint returns boolean
|
|
- Export buffer has exact 5-column contract; DkvMailService uses runtime transport
|
|
- MailModule factory reads DB SmtpConfig first, env fallback second (D-06)
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- DKV-05 SMTP storage backend complete (encrypted, ADMIN-only, password never returned, connection-testable)
|
|
- DKV-04 export + delivery primitives complete (xlsx 5-column buffer, 10-file prune, runtime SMTP send)
|
|
- D-06 closed: existing MailModule sources its SMTP transport from the DB with env fallback
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Create `.planning/phases/07-dkv-fleet-module/07-03-SUMMARY.md` when done. Record how the user-files/ path is resolved, the exact prune ordering used, and how the MailModule factory resolves the DB-vs-env transport (including the tenant chosen by getStartupSmtpConfig).
|
|
</output>
|