feat(12-04): tender-radar notification-pref service + routes, instantAlert passthrough
- TenderNotificationPrefService: per-user digestInterval CRUD (default
'daily', upsert on @@unique userId, D-01/D-03)
- UpdateNotificationPrefDto: @IsIn(['daily','weekly','off']) validation (V5)
- GET/PUT /modules/tender-radar/notification-pref, declared before
@Get(':id') (route-order pitfall)
- instantAlert passthrough in Create/UpdateSavedSearchDto and
TenderSavedSearchService.create/update (NOTIFY-02, D-04)
- All pref/profile routes scoped strictly via extractTriageContext(req),
never from body/query (T-12-14, IDOR)
- Updated tenders.controller.spec.ts fakes for the new constructor param
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import { IsIn } from 'class-validator';
|
||||
|
||||
/**
|
||||
* Body DTO for PUT /modules/tender-radar/notification-pref (NOTIFY-01, D-01/
|
||||
* D-03).
|
||||
*
|
||||
* Security (T-12-14 / V4 — IDOR): deliberately has NO userId or tenantId
|
||||
* field — both are always derived server-side from the auth context
|
||||
* (TendersController.extractTriageContext pattern), never trusted from the
|
||||
* request body. Same convention as CreateSavedSearchDto.
|
||||
*
|
||||
* `digestInterval` is validated via @IsIn to exactly the three values the
|
||||
* digest scheduler's due-check logic understands (T-12-15 / V5) — any other
|
||||
* string is rejected by class-validator before it reaches the service.
|
||||
*/
|
||||
export class UpdateNotificationPrefDto {
|
||||
@IsIn(['daily', 'weekly', 'off'])
|
||||
digestInterval!: string;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IsNotEmpty, IsObject, IsOptional, IsString, MaxLength } from 'class-validator';
|
||||
import { IsBoolean, IsNotEmpty, IsObject, IsOptional, IsString, MaxLength } from 'class-validator';
|
||||
|
||||
/**
|
||||
* Body DTO for POST /modules/tender-radar/saved-searches (FILTER-06).
|
||||
@@ -21,6 +21,15 @@ export class CreateSavedSearchDto {
|
||||
|
||||
@IsObject()
|
||||
filters!: Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Sofort-Alert-Toggle (NOTIFY-02, D-04) — default false when omitted
|
||||
* (Prisma column default). Optional so existing callers that don't know
|
||||
* about this field keep working unchanged.
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
instantAlert?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,4 +47,9 @@ export class UpdateSavedSearchDto {
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
filters?: Record<string, unknown>;
|
||||
|
||||
/** Sofort-Alert-Toggle (NOTIFY-02, D-04) — see CreateSavedSearchDto. */
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
instantAlert?: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user