Files
tessera-ctl/apps/api/src/tenders/dto/tender-email-config.dto.ts
T
schalli 1be6b15249 feat(14-03): add per-tenant encrypted TenderEmailConfig + ownerTenantId write-side (D-13)
Prisma: new TenderEmailConfig model (per-tenant, tenantId @unique, mirrors
DkvModuleConfig) + Tender.ownerTenantId nullable column + index (D-13:
null = global/platform-wide, unchanged for all existing rows and every
public source; set = visible only to that tenant). Migration
20260723113917_tender_email_config_owner_tenant_id applied locally.

TenderEmailConfigService: safe-select admin CRUD (GET never returns the
password, only hasPassword — T-07-12) with DkvService's encrypt-preserve-
empty semantics, via CalendarCryptoService (AES-256-GCM).

RawTenderRecord/NormalizedTenderFields gain optional ownerTenantId,
threaded through TenderNormalizerService.assemble() unchanged.
TenderDedupService's CREATE branch writes ownerTenantId (defaulting to
null); the UPDATE branch deliberately never references it, so a tender
later also seen on a public source is never retroactively hidden.

EmailAlertAdapter.fetchTenders() now does the real per-tenant fan-out:
findMany({isActive:true}) across ALL tenants (deliberate, documented
cross-tenant platform-scheduler read, never forTenant()/RLS), decrypts
each tenant's credentials, picks imap/exchange provider, and tags every
extracted candidate with ownerTenantId — catch-per-tenant so one broken
mailbox never blocks the others.

tenders.module.ts: imports CalendarModule/InboxModule, registers
EmailAlertAdapter + TenderEmailConfigService, seeds an 'email-alert'
TenderSourcePollConfig row (pollGranularity='tick', isActive=false —
no default mailbox to activate yet, D-02 framework-ready stance).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:45:11 +02:00

80 lines
2.0 KiB
TypeScript

import {
IsBoolean,
IsEmail,
IsIn,
IsInt,
IsOptional,
IsString,
Max,
Min,
} from 'class-validator';
/**
* DTO for creating or updating the per-tenant TenderEmailConfig (Phase 14,
* Plan 03, INGEST-05/CONFIG-02, D-06/D-07). Mirrors DkvConfigDto's mailbox
* fields exactly — this is a SEPARATE, tenant-scoped alert mailbox, not the
* DKV invoice inbox (D-03).
*
* Security:
* - T-14-03-02: senderFilter validated as email address (injection mitigation)
* - T-14-03-02: port constrained to 1-65535
* - T-14-03-02: protocol/encryption constrained with @IsIn
*/
export class TenderEmailConfigDto {
/** Inbox protocol — 'imap' for IMAP, 'exchange' for Exchange (EWS). */
@IsIn(['imap', 'exchange'])
protocol!: string;
/** Mail server hostname/IP (IMAP) or full EWS endpoint URL (Exchange). */
@IsOptional()
@IsString()
host?: string;
/** TCP port. Standard values: 993 (IMAP SSL/TLS), 143 (IMAP STARTTLS), 443 (EWS). */
@IsOptional()
@IsInt()
@Min(1)
@Max(65535)
port?: number;
/** TLS mode: 'none' | 'starttls' | 'ssl-tls'. */
@IsIn(['none', 'starttls', 'ssl-tls'])
encryption!: string;
/** IMAP folder to monitor (e.g. "INBOX"). Exchange resolves via display name. */
@IsOptional()
@IsString()
folder?: string;
/**
* Sender email address to filter by. Validated as email address to
* prevent header injection (mirrors DkvConfigDto / T-07-04).
*/
@IsOptional()
@IsEmail()
senderFilter?: string;
/** Exchange only: Windows domain (optional). */
@IsOptional()
@IsString()
domain?: string;
/** Inbox username (stored encrypted; blank on load, T-07-12). */
@IsOptional()
@IsString()
username?: string;
/**
* Inbox password (stored encrypted; blank on load).
* T-07-12: never returned to the frontend in responses.
*/
@IsOptional()
@IsString()
password?: string;
/** Whether the email-alert poll is active for this tenant's mailbox. */
@IsOptional()
@IsBoolean()
isActive?: boolean;
}