adb72f611f
- 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
55 lines
1.7 KiB
TypeScript
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';
|
|
}
|