feat(quick-260907-let): Verbindungstest fuer Postfach-Endpunkt im API
- TenderEmailConfigService.testConnection(userId, dto) mit Rueckfall auf
gespeicherte, entschluesselte Zugangsdaten bei leeren Feldern
- TendersController: POST email-config/test, userId aus Auth-Kontext,
deklariert vor @Get(':id')
- Beide Provider (ImapProvider/ExchangeInboxProvider) optional angehaengt,
bestehende 2-Arg-Konstruktoraufrufe bleiben typkorrekt
- Reihenfolge-Waechter und IDOR-Testfall ergaenzt
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger, Optional } from '@nestjs/common';
|
||||
import { CryptoService } from '../crypto/crypto.service';
|
||||
import { ExchangeInboxProvider } from '../inbox/exchange-inbox.provider';
|
||||
import { ImapProvider } from '../inbox/imap.provider';
|
||||
import type { InboxConfig, InboxProvider } from '../inbox/inbox-provider.interface';
|
||||
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import type { TenderEmailConfigDto } from './dto/tender-email-config.dto';
|
||||
@@ -59,9 +62,21 @@ const EMAIL_CONFIG_SAFE_SELECT = {
|
||||
*/
|
||||
@Injectable()
|
||||
export class TenderEmailConfigService {
|
||||
private readonly logger = new Logger(TenderEmailConfigService.name);
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly crypto: CryptoService,
|
||||
/**
|
||||
* Appended as OPTIONAL trailing constructor params (Quick 260907-let) —
|
||||
* preserves every existing `new TenderEmailConfigService(prisma,
|
||||
* crypto)` 2-arg call site in the spec unchanged. NestJS DI always
|
||||
* resolves and injects both in production (InboxModule is imported in
|
||||
* tenders.module.ts and exports both providers) — same pattern as
|
||||
* TendersController.tenderIngestionService (tenders.controller.ts).
|
||||
*/
|
||||
@Optional() private readonly imapProvider?: ImapProvider,
|
||||
@Optional() private readonly exchangeProvider?: ExchangeInboxProvider,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -164,4 +179,65 @@ export class TenderEmailConfigService {
|
||||
select: EMAIL_CONFIG_SAFE_SELECT,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the inbox connection using credentials from the form DTO,
|
||||
* WITHOUT persisting anything (structural clone of
|
||||
* DkvService.testConnection, dkv.service.ts:201).
|
||||
*
|
||||
* Credential fallback (T-QT16-03): a DTO field left empty falls back to
|
||||
* this SAME user's stored, decrypted credentials — never another user's
|
||||
* row, since `where: { userId }` is the only lookup key. Unlike
|
||||
* saveConfig's credChanged branching, BOTH username and password are
|
||||
* independently backfilled here (saveConfig only ever needs to backfill
|
||||
* the one field the caller didn't touch on a partial re-save; a
|
||||
* connection test with a fully blank form needs both).
|
||||
*
|
||||
* T-05-13: decrypted credentials exist only within this method's local
|
||||
* scope — never returned, never logged beyond the userId itself.
|
||||
*/
|
||||
async testConnection(
|
||||
userId: string,
|
||||
dto: TenderEmailConfigDto,
|
||||
): Promise<{ success: boolean; message?: string }> {
|
||||
let username: string | undefined = dto.username;
|
||||
let password: string | undefined = dto.password;
|
||||
|
||||
if (!username || !password) {
|
||||
try {
|
||||
const existing = await this.prisma.tenderEmailConfig.findUnique({ where: { userId } });
|
||||
if (existing?.encryptedInboxCreds) {
|
||||
const stored = JSON.parse(this.crypto.decrypt(existing.encryptedInboxCreds)) as {
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
if (!username) username = stored.username;
|
||||
if (!password) password = stored.password;
|
||||
}
|
||||
} catch {
|
||||
// T-05-13: generic — no credential details, only the userId
|
||||
this.logger.error(`testConnection: failed to load stored credentials for user ${userId}`);
|
||||
}
|
||||
}
|
||||
|
||||
const inboxConfig: InboxConfig = {
|
||||
protocol: dto.protocol,
|
||||
host: dto.host ?? '',
|
||||
port: dto.port ?? 993,
|
||||
username,
|
||||
password,
|
||||
encryption: dto.encryption,
|
||||
folder: dto.folder ?? 'INBOX',
|
||||
senderFilter: dto.senderFilter,
|
||||
domain: dto.domain,
|
||||
};
|
||||
|
||||
const provider: InboxProvider | undefined =
|
||||
dto.protocol === 'exchange' ? this.exchangeProvider : this.imapProvider;
|
||||
if (!provider) {
|
||||
return { success: false, message: 'Inbox-Anbieter nicht verfuegbar' };
|
||||
}
|
||||
|
||||
return provider.testConnection(inboxConfig);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user