feat(quick-260914-eym): Mail-Transport je Versand nach Mandant (WINDOWS #30), ldap/digest/matching ueber Systemkontext, vier Tabellen im Werkzeug, Erlaubnisliste vollstaendig
- mail: MailerModule-Fabrik und DB-Startpfad (findFirst beim Boot) ersatzlos entfernt; MailService baut je Versand einen nodemailer-Transport aus getDecryptedSmtpConfig(tenantId) des Empfaenger-Mandanten, Umgebungs-Kette (MAIL_* -> TESSERA_SMTP_* -> localhost:1025) nur als Rueckfall; Fehler weiter verschluckt (T-02-12), close() im finally; neue mail.service.spec.ts (4 Tests, T-GWH-03 geschlossen) - settings: Startpfad-Methode samt vier Spec-Tests geloescht; auth: requestPasswordReset reicht user.tenantId durch (Spec-Zusicherung) - ldap: getAllActiveConfigs und Nachverschluesselung lesen ueber forSystem (zwei Zuweisungen), Schreibzeile je Altzeile ueber forTenant(config.tenantId); Tests 301/306 umgedreht, neuer Altzeilen-Test - tender-digest: Kandidatenabfrage ueber forSystem, Schleife gebunden (+1 Test) - tender-matching: Profilabfrage ueber forSystem, Katalog (D-03) ungebunden (+1 Test) - tender-notifications.integration.spec: Mock um forSystem - Werkzeug: LdapConfig (15 Spalten), LdapFieldMapping (6), TenderMatch (8), TenderSavedSearch (8) je neun Kennungen plus Relations-Kennung ldapconfig-systemkontext-include-fieldmappings-beider-mandanten -> Alle 253 Pruefungen bestanden - Detektor: FORSYSTEM_ALLOWED_CALL_SITES auf 4 Dateien / 5 Aufrufe; Proben-Empfaenger sysPrisma (Gate-Zaehlung, Name nicht hartkodiert) - Klassifikation: 6 Zeilen system-gebunden, settings/smtpConfig gebunden - Falsifizierung durch Rueckbau ausgefuehrt und zurueckgenommen: (a) FOR SELECT bei TenderMatch entfernt -> 5 von 253 rot (Insert gelingt, cmd ALL); (b) Regel TenderSavedSearch aus der Datei entfernt -> 1 von 245 rot (Extraktion), lebende DB bleibt bei 34; (c) local=false -> gruen, plus Reset entfernt -> 5 rot (Erben sichtbar); (d) Zahl 0 -> 2 rot, Fremddatei admin-seed -> 3 rot - Baseline: 64 Dateien / 1054 Tests, tsc 0, Werkzeug 253 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
This commit is contained in:
@@ -7,11 +7,13 @@ import { forTenant } from '../prisma/prisma-tenant.extension';
|
||||
* SettingsService.spec — NEU (260911-gwh). Der Bereich `settings` hatte VOR
|
||||
* diesem Lauf KEINE Testdatei (Befund J). Zwei-Klienten-Nachbau, aber mit
|
||||
* einer GRENZE als Bauform (anders als `favorites`): der UNGEBUNDENE Nachbau
|
||||
* bietet fuer `smtpConfig` AUSSCHLIESSLICH `findFirst` (der Startpfad) —
|
||||
* KEIN `findUnique`, KEIN `upsert`; der GEBUNDENE Klient bietet
|
||||
* AUSSCHLIESSLICH `findUnique`/`upsert` — KEIN `findFirst`. Ein gebundener
|
||||
* Startpfad scheitert damit ebenso hart wie ein ungebundener Anfrageweg
|
||||
* ("X is not a function" statt eines stillen Fallbacks).
|
||||
* bietet fuer `smtpConfig` AUSSCHLIESSLICH `findFirst` — KEIN `findUnique`,
|
||||
* KEIN `upsert`; der GEBUNDENE Klient bietet AUSSCHLIESSLICH
|
||||
* `findUnique`/`upsert` — KEIN `findFirst`. Ein ungebundener Anfrageweg
|
||||
* scheitert damit hart ("X is not a function" statt eines stillen
|
||||
* Fallbacks). Der ungebundene Startpfad des Mailmoduls (findFirst beim
|
||||
* Boot) und sein describe-Block sind seit 260914-eym (WINDOWS #30)
|
||||
* GELOESCHT — der ungebundene Nachbau bleibt als Falsifizierungsform stehen.
|
||||
*
|
||||
* `nodemailer` wird per `vi.mock` ersetzt — kein echter Transport (lokal
|
||||
* gibt es keinen `mailhog`).
|
||||
@@ -416,88 +418,6 @@ describe('SettingsService — Bindung an forTenant() (260911-gwh)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadAnySmtpConfigForStartupTransport (Startpfad, bewusst ungebunden)', () => {
|
||||
it('laeuft ueber den UNGEBUNDENEN Nachbau (findFirst), liefert secure/requireTLS/entschluesseltes Kennwort', async () => {
|
||||
const prisma = makeFakePrisma([
|
||||
{
|
||||
id: 'smtp-a',
|
||||
tenantId: 't1',
|
||||
host: 'smtp-a.example.invalid',
|
||||
port: 465,
|
||||
encryption: 'ssl-tls',
|
||||
username: 'user-a',
|
||||
encryptedPassword: 'enc(geheim)',
|
||||
fromAddress: 'a@example.invalid',
|
||||
},
|
||||
]);
|
||||
const crypto = makeFakeCrypto();
|
||||
const service = new SettingsService(prisma as any, crypto as any);
|
||||
|
||||
const result = await service.loadAnySmtpConfigForStartupTransport();
|
||||
|
||||
expect(result).toEqual({
|
||||
host: 'smtp-a.example.invalid',
|
||||
port: 465,
|
||||
secure: true,
|
||||
requireTLS: false,
|
||||
username: 'user-a',
|
||||
password: 'geheim',
|
||||
fromAddress: 'a@example.invalid',
|
||||
});
|
||||
});
|
||||
|
||||
it('requireTLS bei starttls', async () => {
|
||||
const prisma = makeFakePrisma([
|
||||
{
|
||||
id: 'smtp-a',
|
||||
tenantId: 't1',
|
||||
host: 'smtp-a.example.invalid',
|
||||
port: 587,
|
||||
encryption: 'starttls',
|
||||
username: null,
|
||||
encryptedPassword: null,
|
||||
fromAddress: 'a@example.invalid',
|
||||
},
|
||||
]);
|
||||
const service = new SettingsService(prisma as any, makeFakeCrypto() as any);
|
||||
|
||||
const result = await service.loadAnySmtpConfigForStartupTransport();
|
||||
|
||||
expect(result?.secure).toBe(false);
|
||||
expect(result?.requireTLS).toBe(true);
|
||||
});
|
||||
|
||||
it('leerer Nachbau -> null', async () => {
|
||||
const prisma = makeFakePrisma([]);
|
||||
const service = new SettingsService(prisma as any, makeFakeCrypto() as any);
|
||||
|
||||
const result = await service.loadAnySmtpConfigForStartupTransport();
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('Null-Klienten-Nachweis: der Startpfad erzeugt KEINEN gebundenen Klienten (gemessen, nicht behauptet)', async () => {
|
||||
const prisma = makeFakePrisma([
|
||||
{
|
||||
id: 'smtp-a',
|
||||
tenantId: 't1',
|
||||
host: 'smtp-a.example.invalid',
|
||||
port: 587,
|
||||
encryption: 'starttls',
|
||||
username: null,
|
||||
encryptedPassword: null,
|
||||
fromAddress: 'a@example.invalid',
|
||||
},
|
||||
]);
|
||||
const service = new SettingsService(prisma as any, makeFakeCrypto() as any);
|
||||
|
||||
vi.mocked(forTenant).mockClear();
|
||||
await service.loadAnySmtpConfigForStartupTransport();
|
||||
|
||||
expect(vi.mocked(forTenant).mock.calls.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Wachhund je Anfrageweg', () => {
|
||||
const storedRow: FakeSmtpRow = {
|
||||
id: 'smtp-a',
|
||||
|
||||
@@ -91,8 +91,10 @@ export class SettingsService {
|
||||
|
||||
/**
|
||||
* Internal: Get the decrypted SMTP config for a tenant.
|
||||
* Used by DkvMailService/TenderMailService to build a nodemailer transport
|
||||
* at send time — the ONLY send path (Befund K, 260909-laa/260909-mir).
|
||||
* Used by DkvMailService/TenderMailService — and seit 260914-eym auch von
|
||||
* MailService (Systemmails, Transport je Versand nach Mandant des
|
||||
* Empfaengers, WINDOWS #30) — 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
|
||||
@@ -190,75 +192,4 @@ export class SettingsService {
|
||||
return { success: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tenant-agnostic startup accessor for the MailModule factory.
|
||||
*
|
||||
* 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 #30 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 loadAnySmtpConfigForStartupTransport(): Promise<{
|
||||
host: string;
|
||||
port: number;
|
||||
secure: boolean;
|
||||
requireTLS: boolean;
|
||||
username: string | null;
|
||||
password: string | null;
|
||||
fromAddress: string;
|
||||
} | null> {
|
||||
const config = await this.prisma.smtpConfig.findFirst();
|
||||
|
||||
if (!config) return null;
|
||||
|
||||
let password: string | null = null;
|
||||
if (config.encryptedPassword) {
|
||||
// T-07-11: Used only to build transport at startup; never logged
|
||||
password = this.crypto.decrypt(config.encryptedPassword);
|
||||
}
|
||||
|
||||
return {
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
secure: config.encryption === 'ssl-tls',
|
||||
requireTLS: config.encryption === 'starttls',
|
||||
username: config.username,
|
||||
password,
|
||||
fromAddress: config.fromAddress,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user