feat(quick-260911-nke): Benutzer an 34 Aufrufstellen gesetzt, zehn Tabellen gemessen, sechs Pruefungen umgedreht

- 30 verbleibende forTenant()-Aufrufstellen in sieben Diensten (calendar 6,
  dashboard 9, favorites 5, tender-email-config 3, tender-notification-pref 2,
  tender-rss-feed 2, tender-triage 3) reichen userId als drittes Argument
  durch. tender-digest.scheduler.ts bleibt zweistellig (Hintergrunddienst,
  Etappe 3c), mit Begruendung im Kommentar. Keine Methodensignatur, kein
  Controller angefasst, keine anwendungsseitige userId-Filterung entfernt.
- rls-scratch-check.mjs: zwoelf Extraktionsstellen auf die neue Migration
  umgeleitet (TenderEmailConfig/TenderNotificationPref/TenderSavedSearch/
  TenderTriage/TenderRssFeedSource in runTendersAreaChecks, SearchProvider in
  runSearchProviderAreaChecks/runDashboardAreaChecks, DashboardLayout/
  WidgetInstance, CalendarSource/FavoriteLink samt regelstand-eindeutig-Gates).
  SearchProvider/TenderRssFeedSource jetzt mit extractAllPolicySql (4 Regeln).
  runUserDimensionChecks() um die uebrigen neun Tabellen erweitert (neue
  Routine runCommandSeparatedPersonalTableCheck fuer die zwei NULL-faehigen
  Tabellen inkl. gemeinsame-Zeile-Pruefungen).
- Sechs Loch-Pruefungen umgedreht (dashboardlayout, widgetinstance,
  searchprovider, calendarsource, favoritelink-Doppelaussage getrennt) —
  alte Messung ohne Benutzer bleibt unter neuem Namen, Umkehrung MIT
  Benutzer erwartet das Gegenteil; kein alter Name mehr als Kennung.
- Baseline: 1020/62 Tests weiterhin gruen, Typpruefung sauber, Werkzeug
  203/203 bestanden (vorher 146).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
2026-09-11 17:39:17 +02:00
parent f0b531b712
commit 07fc653f52
16 changed files with 785 additions and 108 deletions
@@ -54,6 +54,12 @@ const EMAIL_CONFIG_SAFE_SELECT = {
* uniqueness constraint on `userId` and surfaces as a translated
* ConflictException, not a raw 500 (T-LAA-07, Befund F, Aufgabe 1).
*
* Benutzerdimension seit 20260911120000 (Etappe 3b, 260911-nke): every
* `forTenant()` call above also passes `userId` as the third argument, so
* the `tenant_isolation_policy` on TenderEmailConfig ALSO enforces
* `userId = current_user_id()` — a second net, not a replacement for the
* `userId @unique` ownership model above.
*
* Security:
* - T-07-12: encryptedInboxCreds is excluded from every read-path select;
* getConfigForApi returns `hasPassword: boolean` instead of the password.
@@ -96,7 +102,7 @@ export class TenderEmailConfigService {
* by userId (T-17-01) — a user only ever reads their own mailbox.
*/
async getConfigForApi(userId: string, tenantId: string) {
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
const tenantPrisma = forTenant(this.prisma, tenantId, userId) as any;
const safe = await tenantPrisma.tenderEmailConfig.findUnique({
where: { userId },
select: EMAIL_CONFIG_SAFE_SELECT,
@@ -146,7 +152,7 @@ export class TenderEmailConfigService {
*/
async saveConfig(ctx: { userId: string; tenantId: string }, dto: TenderEmailConfigDto) {
const { userId, tenantId } = ctx;
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
const tenantPrisma = forTenant(this.prisma, tenantId, userId) as any;
let encryptedInboxCreds: string | undefined;
const credChanged =
@@ -234,7 +240,7 @@ export class TenderEmailConfigService {
if (!username || !password) {
try {
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
const tenantPrisma = forTenant(this.prisma, tenantId, userId) as any;
const existing = await tenantPrisma.tenderEmailConfig.findUnique({ where: { userId } });
if (existing?.encryptedInboxCreds) {
const stored = JSON.parse(this.crypto.decrypt(existing.encryptedInboxCreds)) as {