feat(quick-260914-m97): Fehlermeldungen per E-Mail — Empfaenger in SmtpConfig (Migration), MailService-Anhaenge, Modul bug-reports mit Drossel, PNG-Pruefung und Mandant aus der Sitzung
- SmtpConfig.bugReportRecipient (nullable, additive Migration 20260914170000), DTO @IsOptional @IsEmail, SAFE_SELECT, getBugReportRecipient gebunden - MailService: Versandkern deliver (wirft, Anhaenge), sendViaTenantTransport bleibt verschluckender Mantel (T-02-12), sendBugReport laesst Fehler durch - POST /bug-reports: Multipart 4 MiB je Route, alle angemeldeten Rollen, Drossel 5/10 min -> 429, PNG-Signatur -> 400, kein Empfaenger -> 409, Versandfehler -> 502, eine Protokollzeile - Falsifizierungen (a)-(d) als Specs; @Expose() im DTO, damit errors auch bei fehlendem Feld zu [] wird - Doku-Zeile fuer rls-access-inventory, TESSERA_BUGREPORT_TO in docker-compose.prod.yml Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
This commit is contained in:
@@ -37,11 +37,36 @@ import { SettingsService } from '../settings/settings.service';
|
||||
* der Anmeldeweg antwortet weiter 200, keine E-Mail-Enumeration).
|
||||
*
|
||||
* Sicherheit: das entschluesselte Kennwort existiert nur im Rumpf von
|
||||
* `resolveTransport`/`sendViaTenantTransport` und wird nie protokolliert
|
||||
* `resolveTransport`/`deliver` und wird nie protokolliert
|
||||
* (T-07-10/T-07-11); Protokollzeilen nennen nur Quelle (tenant/env) und
|
||||
* Empfaenger.
|
||||
*
|
||||
* Seit quick-260914-m97 (Fehler-melden-Knopf) ist der Versandkern
|
||||
* `deliver` herausgeloest: er WIRFT bei Transportfehlern und kennt
|
||||
* Anhaenge. `sendViaTenantTransport` bleibt der verschluckende Mantel fuer
|
||||
* Kennwort-Reset und Willkommensmail (T-02-12 unveraendert); `sendBugReport`
|
||||
* ruft den Kern direkt, damit der Anwender erfaehrt, ob sein Bericht ankam.
|
||||
*/
|
||||
|
||||
/** Anhang in der nodemailer-Form (`attachments` von `sendMail`). */
|
||||
export interface OutgoingAttachment {
|
||||
filename: string;
|
||||
content: Buffer;
|
||||
contentType: string;
|
||||
}
|
||||
|
||||
/** Eine ausgehende Mail, wie `deliver` sie an nodemailer reicht. */
|
||||
export interface OutgoingMail {
|
||||
to: string;
|
||||
subject: string;
|
||||
text: string;
|
||||
html?: string;
|
||||
attachments?: OutgoingAttachment[];
|
||||
}
|
||||
|
||||
/** Was `BugReportsService` liefert — Empfaenger und Mandant kommen getrennt. */
|
||||
export type BugReportMail = Pick<OutgoingMail, 'subject' | 'text' | 'attachments'>;
|
||||
|
||||
interface ResolvedTransport {
|
||||
source: 'tenant' | 'env';
|
||||
options: nodemailer.TransportOptions & Record<string, unknown>;
|
||||
@@ -128,15 +153,12 @@ export class MailService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Der eine Versandpfad: Transport je Versand aus `resolveTransport`,
|
||||
* Fehler verschluckt und protokolliert (T-02-12), `close()` im `finally`
|
||||
* (WR-01 — keine offenen Verbindungen).
|
||||
* Der eine Versandkern: Transport je Versand aus `resolveTransport`,
|
||||
* `sendMail` mit optionalem HTML und Anhaengen, `close()` im `finally`
|
||||
* (WR-01 — keine offenen Verbindungen). WIRFT bei Transportfehlern —
|
||||
* ob der Fehler nach aussen geht, entscheidet der Aufrufer.
|
||||
*/
|
||||
private async sendViaTenantTransport(
|
||||
tenantId: string,
|
||||
mail: { to: string; subject: string; text: string },
|
||||
kind: string,
|
||||
): Promise<void> {
|
||||
private async deliver(tenantId: string, mail: OutgoingMail, kind: string): Promise<void> {
|
||||
let transport: nodemailer.Transporter | null = null;
|
||||
try {
|
||||
const resolved = await this.resolveTransport(tenantId);
|
||||
@@ -146,19 +168,49 @@ export class MailService {
|
||||
to: mail.to,
|
||||
subject: mail.subject,
|
||||
text: mail.text,
|
||||
...(mail.html !== undefined ? { html: mail.html } : {}),
|
||||
...(mail.attachments !== undefined ? { attachments: mail.attachments } : {}),
|
||||
});
|
||||
this.logger.log(`${kind} email sent to ${mail.to} (transport: ${resolved.source})`);
|
||||
} finally {
|
||||
transport?.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verschluckender Mantel um `deliver` fuer Kennwort-Reset und
|
||||
* Willkommensmail: Fehler werden protokolliert, nie geworfen — der
|
||||
* Anmeldeweg antwortet weiter 200, keine E-Mail-Enumeration (T-02-12
|
||||
* bleibt fuer genau diese beiden Wege bestehen).
|
||||
*/
|
||||
private async sendViaTenantTransport(
|
||||
tenantId: string,
|
||||
mail: { to: string; subject: string; text: string },
|
||||
kind: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.deliver(tenantId, mail, kind);
|
||||
} catch (error) {
|
||||
// Log but don't throw -- caller returns 200 regardless (T-02-12)
|
||||
this.logger.error(
|
||||
`Failed to send ${kind} email to ${mail.to}`,
|
||||
error instanceof Error ? error.stack : String(error),
|
||||
);
|
||||
} finally {
|
||||
transport?.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fehlermeldung eines Anwenders (quick-260914-m97) mit PNG-Anhang an das
|
||||
* eingestellte Postfach des Mandanten. Fehler gehen BEWUSST nach aussen —
|
||||
* anders als bei T-02-12: hier gibt es nichts zu verbergen (kein
|
||||
* Anmeldeweg, kein Enumerationsrisiko), und der Anwender soll wissen, ob
|
||||
* sein Bericht angekommen ist. `BugReportsService` uebersetzt den Fehler
|
||||
* in eine 502-Antwort.
|
||||
*/
|
||||
async sendBugReport(tenantId: string, to: string, report: BugReportMail): Promise<void> {
|
||||
await this.deliver(tenantId, { to, ...report }, 'Bug report');
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a password reset email with a time-limited token link.
|
||||
* T-02-12: The caller always returns 200 regardless of whether this succeeds
|
||||
|
||||
Reference in New Issue
Block a user