feat(260911-gwh): GREEN — favorites/settings binden, Startpfad umbenennen, Widget-Besitzriegel

- favorites.service.ts: alle fuenf Methoden nehmen tenantId als ersten
  Parameter, laufen je ueber EINEN Klienten tenantPrisma (7 gebundene
  Favoritenzugriffe, 5 Aufrufstellen); create() prueft vor der Icon-Suche,
  dass das Ziel-Widget dem Aufrufer gehoert (T-GWH-05, Befund F aus Aufgabe 1
  bestaetigt den Fremdschluessel-Durchgriff) -- Widget not found fuer alle
  drei Faelle (existiert nicht/Kollege/fremder Mandant)
- favorites.controller.ts: reicht tenantId an alle fuenf Aufrufe durch,
  extractContext unveraendert (dashboard-Praezedenzfall)
- settings.service.ts: getSmtpConfig/saveSmtpConfig/getDecryptedSmtpConfig
  je EIN Klient (3 gebundene Zugriffe, 3 Aufrufstellen) -- Befund K damit
  erfuellt; Startpfad umbenannt in loadAnySmtpConfigForStartupTransport(),
  bleibt bewusst ungebunden (sechster Fall der Hintergrunddienst-Falle,
  WINDOWS #TBD-GWH -- Aufgabe 3 vergibt die Nummer)
- mail.module.ts: ruft den umbenannten Startpfad auf, Kommentar nennt beide
  Zustaende statt "single-tenant default"
- Vier Falsifizierungsnachweise durchgefuehrt und zurueckgenommen (siehe
  SUMMARY): (a) 3 Faelle rot, (b) 4 Faelle rot, (c) 9 Faelle rot, (d) 4 Faelle
  rot

Bekannt und erwartet (siehe SUMMARY, Praezedenzfall 260911-fh9): zwischen
dieser Aufgabe und Aufgabe 3 ist rls-access-inventory.spec.ts rot (2
Faelle) -- die Klassifikationstabelle ist noch nicht nachgezogen, das ist
Aufgabe 3.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
2026-09-11 13:57:02 +02:00
parent 8f2c13a35f
commit b5f22e2c4a
4 changed files with 149 additions and 31 deletions
+58 -7
View File
@@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { CryptoService } from '../crypto/crypto.service';
import { PrismaService } from '../prisma/prisma.service';
import { forTenant } from '../prisma/prisma-tenant.extension';
import { SmtpConfigDto } from './dto/smtp-config.dto';
import * as nodemailer from 'nodemailer';
@@ -34,9 +35,13 @@ export class SettingsService {
/**
* Get the SMTP config for a tenant — safe (no password field).
* Returns null when no config row exists for the tenant.
*
* Mandantengebunden (260911-gwh): EIN Klient `tenantPrisma` fuer diese
* Methode, wie die restlichen Anfragewege dieser Datei.
*/
async getSmtpConfig(tenantId: string) {
return this.prisma.smtpConfig.findUnique({
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
return tenantPrisma.smtpConfig.findUnique({
where: { tenantId },
select: {
...SMTP_SAFE_SELECT,
@@ -52,8 +57,11 @@ export class SettingsService {
* When `dto.password` is empty or absent, the existing encrypted password is preserved.
*
* T-07-08: Encryption via CryptoService. Never logs the plaintext password.
* Mandantengebunden (260911-gwh): EIN Klient `tenantPrisma`.
*/
async saveSmtpConfig(tenantId: string, dto: SmtpConfigDto) {
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
// Determine the encrypted password to store
let encryptedPassword: string | undefined;
@@ -71,7 +79,7 @@ export class SettingsService {
...(encryptedPassword !== undefined ? { encryptedPassword } : {}),
};
const result = await this.prisma.smtpConfig.upsert({
const result = await tenantPrisma.smtpConfig.upsert({
where: { tenantId },
create: { tenantId, ...data },
update: data,
@@ -83,8 +91,15 @@ export class SettingsService {
/**
* Internal: Get the decrypted SMTP config for a tenant.
* Used by DkvMailService to build a nodemailer transport at send time.
* Used by DkvMailService/TenderMailService to build a nodemailer transport
* at send time — the ONLY send path (Befund K, 260909-laa/260909-mir).
* NEVER log the decrypted password (T-07-10 / T-05-13).
*
* Mandantengebunden seit 260911-gwh (Aufgabe 2): EIN Klient
* `tenantPrisma`. Vorher lief diese Methode ungebunden — nach dem
* Scharfschalten waere fuer NIEMANDEN mehr eine Mail rausgegangen
* (tender: warn+skip, dkv: throw). Die Reihenfolgebedingung aus (t4)
* Befund K und (d4) ist mit dieser Bindung erfuellt.
*/
async getDecryptedSmtpConfig(tenantId: string): Promise<{
host: string;
@@ -94,7 +109,8 @@ export class SettingsService {
fromAddress: string;
decryptedPassword: string | null;
} | null> {
const config = await this.prisma.smtpConfig.findUnique({
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
const config = await tenantPrisma.smtpConfig.findUnique({
where: { tenantId },
});
@@ -122,6 +138,8 @@ export class SettingsService {
* Returns true on success, false on failure.
*
* T-07-16: Returns only a boolean — no credentials or transport details in the response.
* Kein eigener Datenbankzugriff — greift ueber `getDecryptedSmtpConfig`
* (bereits gebunden) auf gespeicherte Zugangsdaten zurueck.
*/
async testSmtpConfig(
tenantId: string,
@@ -175,13 +193,46 @@ export class SettingsService {
/**
* Tenant-agnostic startup accessor for the MailModule factory.
* Returns the first SmtpConfig row in the DB (single-tenant deployments) with
* the password decrypted. Returns null when no row exists (env-var fallback path).
*
* BLEIBT bewusst UNGEBUNDEN (260911-gwh, sechster Fall der
* Hintergrunddienst-Falle — gleicher Bauart wie
* `DkvService.loadAnyActiveConfigForScheduler()`, WINDOWS #21, siehe
* dessen Kopfkommentar als Vorlage). Zwei Zustaende, beide gehoeren
* genannt:
*
* - HEUTE bereits falsch, nicht nur ungenau: `findFirst()` ohne jede
* Bedingung zieht bei mehreren Mandanten den SMTP-Server und die
* Absenderadresse EINES beliebigen Mandanten fuer ALLE
* Kennwort-Zuruecksetzungs- und Willkommensmails ALLER Mandanten
* (T-GWH-03 — Nutzung fremder Zugangsdaten, nicht nur Sichtbarkeit).
* - NACH DEM SCHARFSCHALTEN (WINDOWS #18) liefert dieselbe Abfrage
* `null`, `mail.module.ts` faellt auf Umgebungsvariablen und zuletzt
* `localhost:1025` zurueck — ein FALSCHER, aber vorhandener Transport
* statt einer Meldung; `MailService` faengt jeden Transportfehler
* (T-02-12) und der Controller antwortet `200`. Das Verstummen ist
* damit DOPPELT verdeckt: erst durch die Rueckfallkette, dann durch
* das Verschlucken im Versand. Das ist die Unsymmetrie zu `ldap`
* (`getAllActiveConfigs`, heute korrekt, verstummt erst spaeter) UND zu
* `dkv` (WINDOWS #21, heute bereits falsch, verstummt spaeter MIT
* Protokollzeile) — hier: heute bereits falsch, verstummt spaeter OHNE
* Protokollzeile.
*
* Binden wuerde diesen Pfad garantiert leer laufen lassen (beim Start
* gibt es strukturell keinen Mandantenkontext). Der Umbau auf Transport
* je Versand aus `getDecryptedSmtpConfig(tenantId)` — die Form, die
* `DkvMailService`/`TenderMailService` bereits haben, `MailService`
* muesste den Mandanten nur von `requestPasswordReset` entgegennehmen —
* ist eine Funktionsaenderung (Umbau des Mailmoduls), KEIN Bindungsumbau,
* NICHT dieser Auftrag. Entscheidung: EIGENER Ledger-Eintrag statt
* Anschluss an #21 (andere Datei, andere Reparatur, andere
* Verdeckungsform) — siehe WINDOWS #TBD-GWH und
* `docs/mandantentrennung-etappe2-fehlerrichtung.md`, Abschnitt
* "## Bereich settings", (s4)(a).
*
* D-06: MailModule reads this at startup (priority 1) and falls back to env vars (priority 2).
* T-07-11: Decrypted password is used only to build the transport — never logged.
*/
async getStartupSmtpConfig(): Promise<{
async loadAnySmtpConfigForStartupTransport(): Promise<{
host: string;
port: number;
secure: boolean;