feat(11-06): saved-searches CRUD routes on TendersController (FILTER-06)
Adds GET/POST /saved-searches and PATCH/DELETE /saved-searches/:searchId,
registers TenderSavedSearchService as a module provider, and wires it into
the controller via extractTriageContext (userId/tenantId from the auth
context, never the body/query — T-11-14/V4 IDOR). Static saved-searches
routes are declared before @Get(':id') (Pitfall 5/T-11-16); mutation routes
use :searchId to avoid ambiguity with the Tender :id param.
Also fixes a Prisma InputJsonValue type mismatch in
TenderSavedSearchService (Rule 1 — caught by tsc --noEmit, same cast
convention as dashboard.service.ts).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
ForbiddenException,
|
||||
Get,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
Req,
|
||||
@@ -14,9 +17,11 @@ import { Request } from 'express';
|
||||
import { Roles } from '../auth/decorators/roles.decorator';
|
||||
import { UseModule } from '../module-registry/module.guard';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreateSavedSearchDto, UpdateSavedSearchDto } from './dto/saved-search.dto';
|
||||
import { SourceConfigDto } from './dto/source-config.dto';
|
||||
import { TenderQueryDto } from './dto/tender-query.dto';
|
||||
import { TenderTriageDto } from './dto/tender-triage.dto';
|
||||
import { TenderSavedSearchService } from './tender-saved-search.service';
|
||||
import { TenderSchedulerService } from './tender-scheduler.service';
|
||||
import { TenderTriageService } from './tender-triage.service';
|
||||
import { buildOrderBy, buildTenderWhere } from './tender-query.builder';
|
||||
@@ -52,6 +57,7 @@ export class TendersController {
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly tenderScheduler: TenderSchedulerService,
|
||||
private readonly tenderTriage: TenderTriageService,
|
||||
private readonly tenderSavedSearch: TenderSavedSearchService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -227,6 +233,74 @@ export class TendersController {
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Saved Searches (per-user, FILTER-06, D-08/D-11) ───────────────────────
|
||||
|
||||
/**
|
||||
* GET /modules/tender-radar/saved-searches — list the current user's
|
||||
* saved search profiles (FILTER-06). Scoped strictly by userId
|
||||
* (T-11-14 / V4 — IDOR), derived from the auth context, never from a
|
||||
* query param.
|
||||
*
|
||||
* MUST be declared before `@Get(':id')` below — same route-order pitfall
|
||||
* as `source-config`/`coverage`/`triage` above (Pitfall 5, T-11-16).
|
||||
*/
|
||||
@Get('saved-searches')
|
||||
@UseModule('tender-radar')
|
||||
async listSavedSearches(@Req() req: Request) {
|
||||
const { userId } = this.extractTriageContext(req);
|
||||
return this.tenderSavedSearch.list(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /modules/tender-radar/saved-searches — create a new saved search
|
||||
* profile. userId/tenantId come exclusively from the auth context
|
||||
* (T-11-14 / V4 — IDOR); `dto` carries only name/filters, never a
|
||||
* userId field.
|
||||
*/
|
||||
@Post('saved-searches')
|
||||
@UseModule('tender-radar')
|
||||
async createSavedSearch(
|
||||
@Body() dto: CreateSavedSearchDto,
|
||||
@Req() req: Request,
|
||||
) {
|
||||
const { userId, tenantId } = this.extractTriageContext(req);
|
||||
return this.tenderSavedSearch.create(userId, tenantId, dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /modules/tender-radar/saved-searches/:searchId — rename and/or
|
||||
* update the filters of an existing saved search. Ownership is verified
|
||||
* in TenderSavedSearchService.update() (T-11-14). Uses `:searchId`
|
||||
* (not `:id`) so this route can never be confused with the Tender
|
||||
* `:id` param below (Pitfall 5).
|
||||
*/
|
||||
@Patch('saved-searches/:searchId')
|
||||
@UseModule('tender-radar')
|
||||
async updateSavedSearch(
|
||||
@Param('searchId') searchId: string,
|
||||
@Body() dto: UpdateSavedSearchDto,
|
||||
@Req() req: Request,
|
||||
) {
|
||||
const { userId } = this.extractTriageContext(req);
|
||||
return this.tenderSavedSearch.update(searchId, userId, dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /modules/tender-radar/saved-searches/:searchId — delete a saved
|
||||
* search. Ownership verified in TenderSavedSearchService.remove()
|
||||
* (T-11-14).
|
||||
*/
|
||||
@Delete('saved-searches/:searchId')
|
||||
@UseModule('tender-radar')
|
||||
async removeSavedSearch(
|
||||
@Param('searchId') searchId: string,
|
||||
@Req() req: Request,
|
||||
) {
|
||||
const { userId } = this.extractTriageContext(req);
|
||||
await this.tenderSavedSearch.remove(searchId, userId);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /modules/tender-radar/:id — single tender detail.
|
||||
* Gated by @UseModule('tender-radar'); NOT scoped by the tenant's id
|
||||
|
||||
Reference in New Issue
Block a user