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'; /** * Safe select for SmtpConfig rows — never returns the encrypted password to API callers. * T-07-07: encryptedPassword is excluded from all GET responses. */ const SMTP_SAFE_SELECT = { id: true, tenantId: true, host: true, port: true, encryption: true, username: true, // encryptedPassword: NEVER included — T-07-07 fromAddress: true, createdAt: true, updatedAt: true, } as const; @Injectable() export class SettingsService { private readonly logger = new Logger(SettingsService.name); constructor( private readonly prisma: PrismaService, private readonly crypto: CryptoService, ) {} /** * 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) { const tenantPrisma = forTenant(this.prisma, tenantId) as any; return tenantPrisma.smtpConfig.findUnique({ where: { tenantId }, select: { ...SMTP_SAFE_SELECT, // Include encryptedPassword presence for hasPassword boolean only encryptedPassword: true, }, }); } /** * Upsert the SMTP config for a tenant. * Encrypts the password with AES-256-GCM when a new password is provided. * 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; if (dto.password && dto.password.length > 0) { encryptedPassword = this.crypto.encrypt(dto.password); // T-07-10: Never log the plaintext password } const data = { host: dto.host, port: dto.port, encryption: dto.encryption, username: dto.username ?? null, fromAddress: dto.fromAddress, ...(encryptedPassword !== undefined ? { encryptedPassword } : {}), }; const result = await tenantPrisma.smtpConfig.upsert({ where: { tenantId }, create: { tenantId, ...data }, update: data, select: SMTP_SAFE_SELECT, }); return result; } /** * Internal: Get the decrypted SMTP config for a tenant. * 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 * `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; port: number; encryption: string; username: string | null; fromAddress: string; decryptedPassword: string | null; } | null> { const tenantPrisma = forTenant(this.prisma, tenantId) as any; const config = await tenantPrisma.smtpConfig.findUnique({ where: { tenantId }, }); if (!config) return null; let decryptedPassword: string | null = null; if (config.encryptedPassword) { // T-05-13: Never log this value decryptedPassword = this.crypto.decrypt(config.encryptedPassword); } return { host: config.host, port: config.port, encryption: config.encryption, username: config.username, fromAddress: config.fromAddress, decryptedPassword, }; } /** * Test an SMTP connection using the submitted DTO. * When `dto.password` is empty, uses the stored decrypted password instead. * 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, dto: SmtpConfigDto, ): Promise<{ success: boolean; warning?: string }> { let password: string | undefined = dto.password; let username: string | undefined = dto.username; // Fall back to stored credentials (form never pre-fills password — T-07-17) if (!password || !username) { const stored = await this.getDecryptedSmtpConfig(tenantId); if (stored) { if (!password) password = stored.decryptedPassword ?? undefined; if (!username) username = stored.username ?? undefined; } } try { const transport = nodemailer.createTransport({ host: dto.host, port: dto.port, secure: dto.encryption === 'ssl-tls', requireTLS: dto.encryption === 'starttls', connectionTimeout: 10_000, greetingTimeout: 10_000, socketTimeout: 10_000, auth: username ? { user: username, pass: password ?? '' } : undefined, }); if (dto.testTo) { await transport.sendMail({ from: dto.fromAddress, to: dto.testTo, subject: 'Tessera SMTP-Test', text: 'Diese E-Mail bestätigt, dass die SMTP-Konfiguration in Tessera funktioniert.', }); } else { await transport.verify(); } return { success: true }; } catch (error) { this.logger.warn( `SMTP connection test failed for tenant ${tenantId}: ${(error as Error).message}`, ); return { success: false }; } } }