feat(11-05): GET/PUT /modules/tender-radar/triage routes + favOnly filter

Adds the batch-triage read/write routes (declared before @Get(':id') per
the route-order pitfall, T-11-13) and wires them through
TenderTriageService with userId/tenantId always derived from the request
context, never the body (T-11-10 / V4 IDOR). Extends TenderQueryDto/
buildTenderWhere with favOnly (UI-04): the controller resolves the
current user's favorited tenderIds server-side before building the
where-clause, and an empty favorites list yields zero matches instead of
the unfiltered catalog. Both batch-ids and favIds in-lists are bounded
(T-11-11 DoS). tenders.controller.spec.ts constructor calls updated for
the new TenderTriageService dependency (Rule 3 — required to keep the
existing suite compiling/passing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-21 16:33:40 +02:00
parent 58b0f3da50
commit 5f97eca804
7 changed files with 371 additions and 16 deletions
+13 -3
View File
@@ -24,9 +24,6 @@ import {
* SORT_MAP, never from a raw user-supplied field name.
*
* Used for: GET /modules/tender-radar?page=1&limit=20&status=active&q=...
*
* favOnly filter is added in a later Phase-11 plan (11-05) — deliberately
* NOT added here.
*/
export class TenderQueryDto {
/**
@@ -166,4 +163,17 @@ export class TenderQueryDto {
@IsArray()
@IsString({ each: true })
cpv?: string[];
/**
* Merklisten-Filter (UI-04, D-10, T-11-10/11). When true, the controller
* resolves the current user's favorited tenderIds via
* `TenderTriageService.favoriteIds(userId)` — derived from the auth
* context, never from this DTO — and passes them into
* `buildTenderWhere(dto, favIds)`. userId itself never appears here
* (V4 / IDOR).
*/
@IsOptional()
@Type(() => Boolean)
@IsBoolean()
favOnly?: boolean;
}
@@ -0,0 +1,27 @@
import { Type } from 'class-transformer';
import { IsBoolean, IsOptional, IsUUID } from 'class-validator';
/**
* Body DTO for PUT /modules/tender-radar/triage.
*
* Security (T-11-10 / V4 — IDOR): this DTO deliberately has NO userId or
* tenantId field — both are always derived server-side from the auth
* context (@CurrentUser-equivalent `req.user`/`req.tenantId`, same
* FavoritesController.extractContext pattern) in TendersController, never
* trusted from the request body.
*/
export class TenderTriageDto {
/** Target tender's id — Tender.id is a `@default(uuid())` string. */
@IsUUID()
tenderId!: string;
@IsOptional()
@Type(() => Boolean)
@IsBoolean()
isRead?: boolean;
@IsOptional()
@Type(() => Boolean)
@IsBoolean()
isFavorite?: boolean;
}