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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user