feat(14-02): add RSS feed admin routes, API client, and settings UI
Adds GET/POST/DELETE /modules/tender-radar/rss-feeds (Roles-guarded
ADMIN/SUPER_ADMIN), declared before the existing @Get(':id') handler to
avoid NestJS route-order shadowing. Delegates to
TenderRssFeedSourceService; the denylist/SSRF rejection (D-14) surfaces
as a 400 unchanged.
Web: tender-radar-api.ts gains listRssFeeds/createRssFeed/deleteRssFeed
(relaying the backend's specific rejection message via
extractErrorMessage), and a new RssFeedListForm client component renders
an "RSS-Feeds" section on the tender-radar settings page (D-09) — list,
add (with inline denylist error), and remove global feed URLs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -21,8 +21,10 @@ import { UpdateNotificationPrefDto } from './dto/notification-pref.dto';
|
||||
import { CreateSavedSearchDto, UpdateSavedSearchDto } from './dto/saved-search.dto';
|
||||
import { SourceConfigDto } from './dto/source-config.dto';
|
||||
import { TenderQueryDto } from './dto/tender-query.dto';
|
||||
import { TenderRssFeedDto } from './dto/tender-rss-feed.dto';
|
||||
import { TenderTriageDto } from './dto/tender-triage.dto';
|
||||
import { TenderNotificationPrefService } from './tender-notification-pref.service';
|
||||
import { TenderRssFeedSourceService } from './tender-rss-feed.service';
|
||||
import { TenderSavedSearchService } from './tender-saved-search.service';
|
||||
import { TenderSchedulerService } from './tender-scheduler.service';
|
||||
import { TenderTriageService } from './tender-triage.service';
|
||||
@@ -61,6 +63,7 @@ export class TendersController {
|
||||
private readonly tenderTriage: TenderTriageService,
|
||||
private readonly tenderSavedSearch: TenderSavedSearchService,
|
||||
private readonly tenderNotificationPref: TenderNotificationPrefService,
|
||||
private readonly tenderRssFeedSource: TenderRssFeedSourceService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -156,6 +159,49 @@ export class TendersController {
|
||||
return config;
|
||||
}
|
||||
|
||||
// ─── RSS-Feeds admin CRUD (Roles-guarded, GLOBAL, D-08/D-14) ───────────────
|
||||
|
||||
/**
|
||||
* GET /modules/tender-radar/rss-feeds — list every admin-managed RSS feed
|
||||
* (global, no tenantId — D-08). `@Roles`-guarded: this is a
|
||||
* platform-admin action, not a per-tenant module feature, same stance as
|
||||
* `source-config` above.
|
||||
*
|
||||
* MUST be declared before `@Get(':id')` below — same route-order pitfall
|
||||
* as `source-config`/`coverage`/`triage`/... above (Pitfall 5): NestJS
|
||||
* matches routes in declaration order, so a `@Get(':id')` placed first
|
||||
* would capture "rss-feeds" as an id and shadow this handler.
|
||||
*/
|
||||
@Get('rss-feeds')
|
||||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||||
async listRssFeeds() {
|
||||
return this.tenderRssFeedSource.list();
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /modules/tender-radar/rss-feeds — add a new global RSS feed URL.
|
||||
* The save-time hostname/SSRF guard (D-14, T-14-02-01) lives in
|
||||
* `TenderRssFeedSourceService.create()` — a denylisted/private-host URL
|
||||
* surfaces as a 400 (BadRequestException) here, unchanged.
|
||||
*/
|
||||
@Post('rss-feeds')
|
||||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||||
async createRssFeed(@Body() dto: TenderRssFeedDto) {
|
||||
return this.tenderRssFeedSource.create(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /modules/tender-radar/rss-feeds/:feedId — remove a global RSS
|
||||
* feed. Uses `:feedId` (not `:id`) so this route can never be confused
|
||||
* with the Tender `:id` route below (Pitfall 5, same convention as
|
||||
* `saved-searches/:searchId`).
|
||||
*/
|
||||
@Delete('rss-feeds/:feedId')
|
||||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||||
async removeRssFeed(@Param('feedId') feedId: string) {
|
||||
return this.tenderRssFeedSource.remove(feedId);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /modules/tender-radar/coverage — distribution of active tenders
|
||||
* by sourcePortal (D-12, UI-05). Feeds the frontend CoverageBanner so a
|
||||
|
||||
Reference in New Issue
Block a user