feat(14-03): add email-config admin routes + D-13 read-side visibility filter + EmailAlertConfigForm
buildTenderWhere gains an optional ownerTenantId param: a resolved
requesting tenant sees global tenders (null) plus its own private ones
(OR[global, mine]); an unresolved requester fails CLOSED to global-only —
never an accidental cross-tenant leak.
TendersController: listTenders/getTender resolve the requesting tenant
leniently from the auth context (resolveRequestingTenantId, never throws)
and apply the D-13 filter; getTender 404s (not a distinct "forbidden") when
a tender's non-null ownerTenantId doesn't match the requester, so no
cross-tenant detail leak. New GET/PUT /modules/tender-radar/email-config
routes (Roles ADMIN/SUPER_ADMIN, tenantId from auth context, never the
body) delegate to TenderEmailConfigService — declared before @Get(':id')
per the project's NestJS route-order convention.
Web: EmailAlertConfig type + fetchEmailConfig/saveEmailConfig client
functions; EmailAlertConfigForm mirrors the DKV InboxConfigForm (password
blank on load, only sent when typed — T-07-12), added as a new
"E-Mail-Alerts" section on the existing tender-radar settings page.
Hardcoded German strings — i18n is Plan 14-05.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -31,10 +31,20 @@ const MAX_FAV_IDS = 500;
|
||||
* `TenderTriageService.favoriteIds(userId)`) from the auth context —
|
||||
* never accepted here as user input. Only consulted when `dto.favOnly` is
|
||||
* true.
|
||||
*
|
||||
* `ownerTenantId` (Phase 14, Plan 03, D-13): the requesting tenant's id,
|
||||
* resolved by the CALLER (TendersController) from the auth context — never
|
||||
* from `dto`. Applies the OR[global, mine] visibility rule for privately-
|
||||
* sourced (email-alert) tenders: `{ OR: [{ownerTenantId:null},
|
||||
* {ownerTenantId}] }`. When the caller cannot resolve a requesting tenant
|
||||
* (no auth context), this fails CLOSED — only globally-visible
|
||||
* (`ownerTenantId: null`) tenders are returned, never a private tenant's
|
||||
* rows leaked to an unidentified requester.
|
||||
*/
|
||||
export function buildTenderWhere(
|
||||
dto: TenderQueryDto,
|
||||
favIds?: string[],
|
||||
ownerTenantId?: string,
|
||||
): Prisma.TenderWhereInput {
|
||||
const where: Prisma.TenderWhereInput = {};
|
||||
const AND: Prisma.TenderWhereInput[] = [];
|
||||
@@ -126,6 +136,16 @@ export function buildTenderWhere(
|
||||
AND.push({ id: { in: ids.length ? ids : ['__none__'] } });
|
||||
}
|
||||
|
||||
// D-13 (Phase 14, Plan 03): private (email-alert) tender visibility.
|
||||
// A resolved requesting tenant sees global tenders (null) PLUS its own;
|
||||
// an unidentified requester (ownerTenantId undefined) sees ONLY global
|
||||
// tenders — fail-closed, never an accidental cross-tenant leak.
|
||||
AND.push(
|
||||
ownerTenantId
|
||||
? { OR: [{ ownerTenantId: null }, { ownerTenantId }] }
|
||||
: { ownerTenantId: null },
|
||||
);
|
||||
|
||||
if (AND.length) where.AND = AND;
|
||||
return where;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user