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:
@@ -48,4 +48,15 @@ export class SmtpConfigDto {
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
testTo?: string;
|
||||
|
||||
/**
|
||||
* Postfach fuer den Fehler-melden-Knopf (quick-260914-m97). Optional:
|
||||
* fehlt das Feld im PUT, bleibt der gespeicherte Wert; `null` loescht ihn.
|
||||
* Absicht (T-M97-05): nur ein Administrator dieses Mandanten kann das
|
||||
* Ziel aller Fehlermeldungen seines Mandanten setzen — der Weg fuehrt
|
||||
* ausschliesslich ueber `PUT /settings/smtp` mit `@Roles(ADMIN, SUPER_ADMIN)`.
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
bugReportRecipient?: string | null;
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ interface FakeSmtpRow {
|
||||
username: string | null;
|
||||
encryptedPassword: string | null;
|
||||
fromAddress: string;
|
||||
bugReportRecipient?: string | null;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
@@ -483,4 +484,71 @@ describe('SettingsService — Bindung an forTenant() (260911-gwh)', () => {
|
||||
expect(vi.mocked(forTenant).mock.calls.length).toBe(0);
|
||||
});
|
||||
});
|
||||
describe('bugReportRecipient — Postfach fuer den Fehler-melden-Knopf (quick-260914-m97)', () => {
|
||||
const rowWithRecipient: FakeSmtpRow = {
|
||||
id: 'smtp-a',
|
||||
tenantId: 't1',
|
||||
host: 'smtp-a.example.invalid',
|
||||
port: 587,
|
||||
encryption: 'starttls',
|
||||
username: 'user-a',
|
||||
encryptedPassword: 'enc(geheim)',
|
||||
fromAddress: 'a@example.invalid',
|
||||
bugReportRecipient: 'fehler@a.example.invalid',
|
||||
};
|
||||
|
||||
it('Test A: getBugReportRecipient liefert den gespeicherten Wert ueber GENAU EINEN gebundenen findUnique; fremder Mandant oder null-Feld -> null', async () => {
|
||||
const prisma = makeFakePrisma([
|
||||
rowWithRecipient,
|
||||
{ ...rowWithRecipient, id: 'smtp-c', tenantId: 't3', bugReportRecipient: null },
|
||||
]);
|
||||
const service = new SettingsService(prisma as any, makeFakeCrypto() as any);
|
||||
|
||||
vi.mocked(forTenant).mockClear();
|
||||
const found = await service.getBugReportRecipient('t1');
|
||||
expect(found).toBe('fehler@a.example.invalid');
|
||||
expectBoundCall(prisma, 't1', 'findUnique');
|
||||
expect(vi.mocked(forTenant).mock.calls.length).toBe(1);
|
||||
|
||||
expect(await service.getBugReportRecipient('t2')).toBeNull();
|
||||
expect(await service.getBugReportRecipient('t3')).toBeNull();
|
||||
});
|
||||
|
||||
it('Test B: saveSmtpConfig mit bugReportRecipient speichert den Wert; Rueckgabe traegt bugReportRecipient und KEIN encryptedPassword', async () => {
|
||||
const prisma = makeFakePrisma();
|
||||
const service = new SettingsService(prisma as any, makeFakeCrypto() as any);
|
||||
|
||||
const result = await service.saveSmtpConfig('t1', {
|
||||
host: 'smtp-a.example.invalid',
|
||||
port: 587,
|
||||
encryption: 'starttls',
|
||||
fromAddress: 'a@example.invalid',
|
||||
bugReportRecipient: 'fehler@a.example.invalid',
|
||||
} as any);
|
||||
|
||||
expect(prisma.__configs.get('t1').bugReportRecipient).toBe('fehler@a.example.invalid');
|
||||
expect((result as any).bugReportRecipient).toBe('fehler@a.example.invalid');
|
||||
expect((result as any).encryptedPassword).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Test C: DTO OHNE das Feld bewahrt den gespeicherten Wert, DTO mit null loescht ihn', async () => {
|
||||
const prisma = makeFakePrisma();
|
||||
const service = new SettingsService(prisma as any, makeFakeCrypto() as any);
|
||||
const base = {
|
||||
host: 'smtp-a.example.invalid',
|
||||
port: 587,
|
||||
encryption: 'starttls',
|
||||
fromAddress: 'a@example.invalid',
|
||||
};
|
||||
|
||||
await service.saveSmtpConfig('t1', { ...base, bugReportRecipient: 'fehler@a.example.invalid' } as any);
|
||||
expect(prisma.__configs.get('t1').bugReportRecipient).toBe('fehler@a.example.invalid');
|
||||
|
||||
await service.saveSmtpConfig('t1', { ...base } as any);
|
||||
expect(prisma.__configs.get('t1').bugReportRecipient).toBe('fehler@a.example.invalid');
|
||||
|
||||
await service.saveSmtpConfig('t1', { ...base, bugReportRecipient: null } as any);
|
||||
expect(prisma.__configs.get('t1').bugReportRecipient).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ const SMTP_SAFE_SELECT = {
|
||||
username: true,
|
||||
// encryptedPassword: NEVER included — T-07-07
|
||||
fromAddress: true,
|
||||
bugReportRecipient: true, // Postfach fuer den Fehler-melden-Knopf (quick-260914-m97)
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
} as const;
|
||||
@@ -77,6 +78,10 @@ export class SettingsService {
|
||||
username: dto.username ?? null,
|
||||
fromAddress: dto.fromAddress,
|
||||
...(encryptedPassword !== undefined ? { encryptedPassword } : {}),
|
||||
// quick-260914-m97: fehlendes Feld = bewahren, null/leer = loeschen
|
||||
...(dto.bugReportRecipient !== undefined
|
||||
? { bugReportRecipient: dto.bugReportRecipient || null }
|
||||
: {}),
|
||||
};
|
||||
|
||||
const result = await tenantPrisma.smtpConfig.upsert({
|
||||
@@ -89,6 +94,24 @@ export class SettingsService {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Postfach fuer den Fehler-melden-Knopf (quick-260914-m97): der Wert aus
|
||||
* `SmtpConfig.bugReportRecipient` des Mandanten oder `null`, wenn keine
|
||||
* Zeile existiert oder das Feld leer ist. Verwender: `BugReportsService`
|
||||
* (der dort den Umgebungs-Rueckfall `TESSERA_BUGREPORT_TO` anhaengt).
|
||||
*
|
||||
* Mandantengebunden: EIN Klient `tenantPrisma`, `findUnique` mit
|
||||
* schmalem `select` — das verschluesselte Kennwort wird hier nie geladen.
|
||||
*/
|
||||
async getBugReportRecipient(tenantId: string): Promise<string | null> {
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||
const row = await tenantPrisma.smtpConfig.findUnique({
|
||||
where: { tenantId },
|
||||
select: { bugReportRecipient: true },
|
||||
});
|
||||
return row?.bugReportRecipient ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal: Get the decrypted SMTP config for a tenant.
|
||||
* Used by DkvMailService/TenderMailService — and seit 260914-eym auch von
|
||||
|
||||
Reference in New Issue
Block a user