1be6b15249
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>
80 lines
2.0 KiB
TypeScript
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;
|
|
}
|