feat(laa-02): binde die fuenf Nutzer-CRUD-Dienste des Bereichs tenders an forTenant()
tender-saved-search.service.ts, tender-triage.service.ts, tender-notification-pref.service.ts und tender-email-config.service.ts laufen jetzt vollstaendig ueber forTenant() — vier neue Parameter (list, update, remove, listForUser, favoriteIds, getForUser, getConfigForApi, testConnection bekommen tenantId), die anwendungsseitige userId-Filterung bleibt unveraendert (Befund E: die Policies haben keine Benutzerdimension). tender-rss-feed.service.ts bindet nur createForUser (Zaehler + Anlage, beide ausschliesslich auf persoenlichen Zeilen); listForUser, createPlatform und remove bleiben mit Codekommentar bewusst ungebunden (WINDOWS #19 — eine gebundene plattformweite Zeile waere unter jedem Mandanten unsichtbar, ein gebundenes Einfuegen ohne Mandant wuerde abgewiesen). tender-notification-pref.service.ts und tender-email-config.service.ts uebersetzen eine P2002-Verletzung auf dem tenantlosen upsert-Schluessel (Befund F) in eine verstaendliche deutsche Meldung statt eines rohen Fehlers. tenders.controller.ts reicht tenantId an den acht betroffenen Aufrufstellen durch extractTriageContext() durch (kein neuer Aufloesungsweg); die drei RSS-Aufrufstellen bleiben unveraendert, da ihre Dienstmethoden nicht binden. Alle sieben angefassten Testdateien bekommen den Zwei-Client-Nachweis (__makeBoundClient ueber demselben Speicher) und Bindungstests je umgestellter Methode; tender-rss-feed.service.spec.ts zusaetzlich den Gegentest, dass die drei unveraendert bleibenden Pfade forTenant() NICHT aufrufen. Falsifiziert: ein probeweiser Rueckbau der list()-Bindung in tender-saved-search.service.ts machte genau den erwarteten Bindungstest rot, danach zurueckgenommen. docs/mandantentrennung-zugriffsklassifikation.md: Stand der fuenf Paare auf gebunden bzw. gemischt nachgezogen; tenderRssFeedSource von muss-mandantengebunden auf beides umklassifiziert (derselbe Praezedenzfall wie ldapConfig in 260909-ipc). 761 Tests gruen (743 + 18 neue Bindungsnachweise), Typpruefung sauber, Wegwerf-Werkzeug 32/32, kein Schema-/Migrations-/Compose-/ Umgebungsdatei-Diff. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { forTenant } from '../prisma/prisma-tenant.extension';
|
||||
|
||||
/**
|
||||
* Partial triage update accepted by setTriage(). Both fields are optional
|
||||
@@ -15,10 +16,14 @@ export interface SetTriageInput {
|
||||
* Service for managing per-user Tender triage state (gelesen/ungelesen,
|
||||
* Favorit — UI-03/04, D-09/D-10/D-11).
|
||||
*
|
||||
* Access control (T-11-10 / V4 — IDOR): every query is scoped by userId,
|
||||
* exactly the `FavoritesService` convention (T-08-06) — NOT `forTenant()`/
|
||||
* RLS (Pitfall 4). userId must always be derived from the caller's auth
|
||||
* context (controller), never accepted as a body/query parameter here.
|
||||
* Access control (T-11-10 / V4 — IDOR): every query is ADDITIONALLY scoped
|
||||
* by userId, exactly the `FavoritesService` convention (T-08-06). This
|
||||
* stays deliberate belt-and-suspenders after binding to `forTenant()`
|
||||
* (260909-laa): the delivered `tenant_isolation_policy` on TenderTriage
|
||||
* has no user dimension — a foreign user of the SAME tenant is not
|
||||
* excluded by the database alone (Befund E, measured for the sibling
|
||||
* TenderSavedSearch policy in Aufgabe 1; all five policies of this area
|
||||
* share the identical `"tenantId" = current_tenant_id()` text).
|
||||
*
|
||||
* Cascade (Pitfall 6): the schema's `Tender @relation(..., onDelete:
|
||||
* Cascade)` removes a tender's triage rows automatically when Phase 10's
|
||||
@@ -53,7 +58,8 @@ export class TenderTriageService {
|
||||
update.favoritedAt = dto.isFavorite ? now : null;
|
||||
}
|
||||
|
||||
return this.prisma.tenderTriage.upsert({
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||
return tenantPrisma.tenderTriage.upsert({
|
||||
where: { userId_tenderId: { userId, tenderId } },
|
||||
update,
|
||||
create: {
|
||||
@@ -74,11 +80,13 @@ export class TenderTriageService {
|
||||
* Scoped by userId (V4/IDOR) — a foreign userId never sees another
|
||||
* user's rows, even for the same tenderId. Returns [] without querying
|
||||
* prisma when tenderIds is empty (avoids an unbounded `in: []` no-op
|
||||
* round-trip).
|
||||
* round-trip) — deliberately BEFORE forTenant() is created, so an empty
|
||||
* batch never even opens a bound client.
|
||||
*/
|
||||
async listForUser(userId: string, tenderIds: string[]) {
|
||||
async listForUser(userId: string, tenantId: string, tenderIds: string[]) {
|
||||
if (!tenderIds.length) return [];
|
||||
return this.prisma.tenderTriage.findMany({
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||
return tenantPrisma.tenderTriage.findMany({
|
||||
where: { userId, tenderId: { in: tenderIds } },
|
||||
});
|
||||
}
|
||||
@@ -88,11 +96,12 @@ export class TenderTriageService {
|
||||
* Merklisten-Filter). Scoped by userId — feeds the favOnly branch of
|
||||
* tender-query.builder.ts's buildTenderWhere.
|
||||
*/
|
||||
async favoriteIds(userId: string): Promise<string[]> {
|
||||
const rows = await this.prisma.tenderTriage.findMany({
|
||||
async favoriteIds(userId: string, tenantId: string): Promise<string[]> {
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||
const rows = await tenantPrisma.tenderTriage.findMany({
|
||||
where: { userId, isFavorite: true },
|
||||
select: { tenderId: true },
|
||||
});
|
||||
return rows.map((r) => r.tenderId);
|
||||
return rows.map((r: { tenderId: string }) => r.tenderId);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user