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:
@@ -13,12 +13,29 @@ import type { TenderQueryDto } from './dto/tender-query.dto';
|
||||
* orderBy key.
|
||||
*/
|
||||
|
||||
/**
|
||||
* T-11-11 (DoS): bounds the favOnly `id: { in: [...] }` list — a user's
|
||||
* own favorites are already implicitly bounded by their behavior, but a
|
||||
* hard cap keeps the generated query's IN-list size predictable
|
||||
* regardless of how many rows accumulate over time.
|
||||
*/
|
||||
const MAX_FAV_IDS = 500;
|
||||
|
||||
/**
|
||||
* buildTenderWhere — conditional AND-composition. Empty DTO fields never
|
||||
* add a constraint; every non-empty field is a correctness/security
|
||||
* requirement documented inline (D-01/04/05).
|
||||
*
|
||||
* `favIds` (UI-04, T-11-10): the current user's favorited tenderIds,
|
||||
* resolved by the CALLER (TendersController, via
|
||||
* `TenderTriageService.favoriteIds(userId)`) from the auth context —
|
||||
* never accepted here as user input. Only consulted when `dto.favOnly` is
|
||||
* true.
|
||||
*/
|
||||
export function buildTenderWhere(dto: TenderQueryDto): Prisma.TenderWhereInput {
|
||||
export function buildTenderWhere(
|
||||
dto: TenderQueryDto,
|
||||
favIds?: string[],
|
||||
): Prisma.TenderWhereInput {
|
||||
const where: Prisma.TenderWhereInput = {};
|
||||
const AND: Prisma.TenderWhereInput[] = [];
|
||||
|
||||
@@ -98,6 +115,17 @@ export function buildTenderWhere(dto: TenderQueryDto): Prisma.TenderWhereInput {
|
||||
AND.push({ cpvDivisions: { hasSome: dto.cpv } });
|
||||
}
|
||||
|
||||
// UI-04 / D-10 (Merklisten-Filter, Pflichtkriterium): favOnly restricts
|
||||
// the result to the current user's favorited tenders. Empty favIds (no
|
||||
// favorites yet, or favIds not supplied) MUST yield ZERO matches, never
|
||||
// "all tenders" — `'__none__'` is a sentinel that can never equal a real
|
||||
// Tender.id (uuid), so `{ in: ['__none__'] }` is a guaranteed-empty
|
||||
// match rather than an accidentally-unconstrained query.
|
||||
if (dto.favOnly) {
|
||||
const ids = (favIds ?? []).slice(0, MAX_FAV_IDS);
|
||||
AND.push({ id: { in: ids.length ? ids : ['__none__'] } });
|
||||
}
|
||||
|
||||
if (AND.length) where.AND = AND;
|
||||
return where;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user