Files
tessera-ctl/apps/api/src/tenders/dto/tender-rss-feed.dto.ts
T
schalli adb72f611f feat(17-02): RSS feeds get an owner — platform-wide vs personal (D-02)
- TenderRssFeedSource.userId/tenantId (nullable): null = platform-wide
  (admin-managed, includes the existing service.bund.de default),
  set = personal feed owned by exactly one user
- Migration replaces url @unique with @@unique([userId, url]) — two
  users can now follow the same address independently; existing rows
  keep an empty owner (platform-wide, unchanged behavior)
- Service: listForUser/createForUser/createPlatform replace list/create
- Controller: GET/POST /rss-feeds move from @Roles(ADMIN,SUPER_ADMIN) to
  @UseModule('tender-radar'); POST with scope:'platform' still requires
  ADMIN/SUPER_ADMIN, checked inline (T-17-08)
- tenders.module.ts seed switched from upsert-on-url to find-then-create
  (Rule 3, pulled forward from Task 3): the new compound unique index
  requires a non-null userId in Prisma's generated type, so a
  platform-wide row can no longer be addressed via upsert

- Files modified: apps/api/prisma/schema.prisma, apps/api/prisma/migrations/20260812110000_tender_rss_feed_owner/migration.sql, apps/api/src/tenders/tender-rss-feed.service.ts, apps/api/src/tenders/dto/tender-rss-feed.dto.ts, apps/api/src/tenders/tenders.controller.ts, apps/api/src/tenders/tenders.module.ts, apps/api/src/tenders/tender-rss-feed.service.spec.ts, apps/api/src/tenders/tenders.controller.spec.ts
2026-08-12 11:37:56 +02:00

55 lines
1.7 KiB
TypeScript

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';
}