import { IsBoolean, IsIn, IsOptional, IsString, IsUrl, MaxLength, MinLength, } from 'class-validator'; /** * DTO for RSS feed sources (`TenderRssFeedSource`, D-14/D-08; ownership * split personal/platform-wide since Phase 17, Plan 02, D-02). * * `@IsUrl` here is only a COARSE well-formedness check (http/https, * protocol required). The SUBSTANTIVE SSRF/denylist guard — rejecting * DENYLISTED_PORTALS hostnames and private/loopback hosts — is enforced in * `TenderRssFeedSourceService.assertUrlAllowed()`, NOT here (RESEARCH.md * Pitfall 3: a user-supplied RSS feed URL is runtime data, added long * after `SourceRegistry.register()`'s DI-boot-time denylist check runs — * this DTO alone provides zero protection against a feed URL pointing at * vergabe24/aumass or an internal host). `require_tld: false` deliberately * lets IP-literal URLs pass THIS validation layer so the service-layer * check can reject them with a clear, domain-specific SSRF error message * instead of a generic "invalid URL" one. */ export class TenderRssFeedDto { @IsUrl({ protocols: ['http', 'https'], require_protocol: true, require_tld: false, }) url!: string; @IsString() @MinLength(1) @MaxLength(200) label!: string; @IsOptional() @IsBoolean() isActive?: boolean; /** * A WISH only, never trusted as authorization by itself (D-02): 'platform' * additionally requires the caller to hold ADMIN/SUPER_ADMIN, enforced * server-side in `TendersController.createRssFeed` — never here or in the * service. Default 'personal' so an ordinary module user's POST creates a * feed they own without needing to know this field exists. */ @IsOptional() @IsIn(['personal', 'platform']) scope?: 'personal' | 'platform'; }