b188946e31
- prisma-tenant.extension.ts: (prisma as any) und die Handannotation an
$allOperations in forTenant()/forSystem() entfernt; Kopfkommentar
unveraendert. .then((results: any[]) => ...) auf unknown[] umgestellt.
- 105 Aufrufstellen `const X = forTenant(...) as any` / `forSystem(...) as
any` von der Zusicherung befreit, Zuweisungsform woertlich erhalten
(rls-access-inventory.spec.ts bleibt scharf, 30/30 gruen einzeln
geprueft).
- withTenantTransaction(): Prisma.TransactionClient fuer tx probiert,
gemessen verworfen - bricht das Testdoppel in
prisma-tenant.extension.spec.ts (TS2322 auf einem absichtlich
unvollstaendigen Fake-Objekt). tx bleibt any, mit Begruendung am Typ.
- Gefolge des jetzt getypten Klienten entfernt: any[]-Annotationen und
.map((x: any) => ...) in groups.service.ts, module-grants.service.ts,
dkv.service.ts, ldap-config.service.ts, tenders.controller.ts:270.
- Befund (D-03): tender-matching.service.ts:159 trug eine Handannotation
(match: { tender: unknown }), die den Wert nur deshalb auf unknown
verengte, um TS7006 unter dem alten any-Klienten zu vermeiden - mit dem
getypten Klienten war das falsch. Annotation geloescht, kein Ersatz
durch Zusicherung.
- Zwei any bleiben gezielt in groups.service.ts (u/a in
ensureDefaultGroup(), gefolge von tx: any) - Begruendung am Code.
noExplicitAny apps/api/src: 288 -> 149 (Schranke 155). type-check 4/4,
lint 5/5 (0 error). apps/api 72/1143 gruen, apps/web 73/531 gruen,
rls-access-inventory.spec.ts 30/30 gruen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
83 lines
3.5 KiB
TypeScript
83 lines
3.5 KiB
TypeScript
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). 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.
|
|
*
|
|
* Nachtrag (260911-nke, Etappe 3b): seit Migration 20260911120000 traegt
|
|
* die Regel auf TenderNotificationPref die Benutzerdimension
|
|
* (`current_user_id() IS NULL OR "userId" = current_user_id()`) — beide
|
|
* `forTenant()`-Aufrufe unten reichen `userId` als drittes Argument durch.
|
|
* Die anwendungsseitige userId-Filterung bleibt zweites Netz, kein Ersatz.
|
|
*/
|
|
@Injectable()
|
|
export class TenderNotificationPrefService {
|
|
constructor(private readonly prisma: PrismaService) {}
|
|
|
|
/**
|
|
* Returns this user's digest interval preference. When no row exists yet
|
|
* (user has never touched the setting), returns the default
|
|
* `{ digestInterval: 'daily' }` (D-01) WITHOUT writing a row — consistent
|
|
* with the digest scheduler's own default-daily due-check semantics, no
|
|
* autowrite needed to represent "using the default".
|
|
*/
|
|
async getForUser(userId: string, tenantId: string): Promise<{ digestInterval: string }> {
|
|
const tenantPrisma = forTenant(this.prisma, tenantId, userId);
|
|
const existing = await tenantPrisma.tenderNotificationPref.findUnique({
|
|
where: { userId },
|
|
});
|
|
|
|
if (!existing) {
|
|
return { digestInterval: 'daily' };
|
|
}
|
|
|
|
return existing;
|
|
}
|
|
|
|
/**
|
|
* Upserts this user's digest interval preference on the @@unique userId
|
|
* (D-03) — a second call for the same user updates the same row rather
|
|
* than creating a new one.
|
|
*/
|
|
async setForUser(userId: string, tenantId: string, digestInterval: string) {
|
|
const tenantPrisma = forTenant(this.prisma, tenantId, userId);
|
|
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;
|
|
}
|
|
}
|
|
}
|