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,19 +1,31 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConflictException, Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { forTenant } from '../prisma/prisma-tenant.extension';
|
||||
|
||||
/**
|
||||
* Service for managing the per-user Tender digest interval preference
|
||||
* (NOTIFY-01, D-01/D-03).
|
||||
*
|
||||
* Access control (T-12-14 / V4 — IDOR): scoped by userId exactly like
|
||||
* TenderSavedSearchService/TenderTriageService (T-11-14/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.
|
||||
* TenderSavedSearchService/TenderTriageService (T-11-14/T-08-06). This
|
||||
* stays deliberate belt-and-suspenders after binding to `forTenant()`
|
||||
* (260909-laa) — the delivered policy on TenderNotificationPref has no
|
||||
* user dimension (Befund E, Aufgabe 1).
|
||||
*
|
||||
* `TenderNotificationPref` has a per-user `@@unique` on `userId` (one row
|
||||
* per user, D-03: the interval is a user setting, not per-profile) — this
|
||||
* service upserts on that key.
|
||||
*
|
||||
* T-LAA-07 (260909-laa, Befund F): `userId` has no tenant dimension. If a
|
||||
* user's stored `tenantId` has gone stale (their resolved tenant changed)
|
||||
* the existing row can become invisible under the now-bound context — a
|
||||
* bound `upsert` then falls into the create branch and hits the
|
||||
* platform-wide uniqueness constraint on `userId`. Aufgabe 1 measured this
|
||||
* exact shape for the sibling `TenderTriage` upsert
|
||||
* (`tendertriage-einfuegen-auf-unsichtbare-zeile-verletzt-eindeutigkeit`):
|
||||
* the failure is a P2002 unique-constraint violation, not an RLS
|
||||
* rejection. Translated below into a German message, same pattern as
|
||||
* `tender-saved-search.service.ts`, instead of surfacing as a raw 500.
|
||||
*/
|
||||
@Injectable()
|
||||
export class TenderNotificationPrefService {
|
||||
@@ -26,8 +38,9 @@ export class TenderNotificationPrefService {
|
||||
* with the digest scheduler's own default-daily due-check semantics, no
|
||||
* autowrite needed to represent "using the default".
|
||||
*/
|
||||
async getForUser(userId: string): Promise<{ digestInterval: string }> {
|
||||
const existing = await this.prisma.tenderNotificationPref.findUnique({
|
||||
async getForUser(userId: string, tenantId: string): Promise<{ digestInterval: string }> {
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||
const existing = await tenantPrisma.tenderNotificationPref.findUnique({
|
||||
where: { userId },
|
||||
});
|
||||
|
||||
@@ -44,10 +57,20 @@ export class TenderNotificationPrefService {
|
||||
* than creating a new one.
|
||||
*/
|
||||
async setForUser(userId: string, tenantId: string, digestInterval: string) {
|
||||
return this.prisma.tenderNotificationPref.upsert({
|
||||
where: { userId },
|
||||
create: { userId, tenantId, digestInterval },
|
||||
update: { digestInterval },
|
||||
});
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||
try {
|
||||
return await tenantPrisma.tenderNotificationPref.upsert({
|
||||
where: { userId },
|
||||
create: { userId, tenantId, digestInterval },
|
||||
update: { digestInterval },
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (error?.code === 'P2002') {
|
||||
throw new ConflictException(
|
||||
'Die Benachrichtigungseinstellung konnte nicht gespeichert werden, weil bereits ein widersprüchlicher Eintrag existiert. Bitte laden Sie die Seite neu und versuchen Sie es erneut.',
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user