9a57fa79f5
Aufgabe 2 der Etappe 2: getConfig/createConfig/updateConfig sowie addFieldMapping/removeFieldMapping laufen jetzt ueber forTenant(), gebunden an den aus der Anfrage bekannten Mandanten. removeFieldMapping nimmt den Mandanten neu als Pflichtparameter entgegen und der Controller holt ihn aus dem Sitzungsnachweis statt nur die URL-Kennung weiterzureichen (T-IPC-01) -- ein Administrator konnte bisher die Feldzuordnung eines fremden Mandanten loeschen, wenn er ihre Kennung kannte. getAllActiveConfigs() und die Start-Nachverschluesselung bleiben bewusst uebergreifend, mit ausgeschriebener Begruendung im Code (Befund B). rls-access-inventory.spec.ts erkennt jetzt neben `this.prisma.<Modell>` auch gebundene `<Name>.<Modell>`-Zugriffe (Befund F/G) und prueft eine neue Stand-Spalte (gebunden/ungebunden/gemischt) im Klassifikationsdokument gegen den Quelltext. Das macht zwei bisher unsichtbare, weil schon laenger gebundene Fundstellen sichtbar (auth.service.ts/passwordResetToken, ldap.service.ts/groupMembership) und deckt auf, dass (ldap-config.service.ts, ldapConfig) tatsaechlich "beides" ist, nicht "muss-mandantengebunden" (Befund B). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
214 lines
21 KiB
Markdown
214 lines
21 KiB
Markdown
# Mandantentrennung — Zugriffsklassifikation
|
||
|
||
Dieses Dokument gehört zusammen mit `docs/mandantentrennung-datenbankrolle.md`
|
||
zur Vorbereitung der Mandantentrennung auf Datenbankebene (WINDOWS #18/#20).
|
||
Während die Datenbankrolle-Anleitung beschreibt, **wie** die Umstellung
|
||
technisch abläuft, hält dieses Dokument fest, **welche** der 227
|
||
`this.prisma.*`-Fundstellen in `apps/api/src` beim Umbau in den folgenden
|
||
Etappen angefasst werden müssen und welche bewusst unverändert bleiben.
|
||
|
||
Die Bestandsaufnahme unten wird **maschinell aus dem Quelltext ermittelt**
|
||
(nicht von Hand zusammengeschrieben) und durch
|
||
`apps/api/src/prisma/rls-access-inventory.spec.ts` bei jedem Testlauf gegen
|
||
den tatsächlichen Stand geprüft. Verschiebt sich eine Zeile, bleibt die
|
||
Prüfung grün — Vergleichsschlüssel sind Datei und Prisma-Modellname, keine
|
||
Zeilennummer. Kommt eine neue Fundstelle hinzu oder verschwindet eine
|
||
bestehende, schlägt die Prüfung fehl, bis dieses Dokument nachgezogen wird.
|
||
|
||
## Die drei Klassen
|
||
|
||
- **`muss-mandantengebunden`** — berührt auf Rechnung genau eines Mandanten
|
||
eine Tabelle mit `tenantId` (oder eine Tabelle, die ihre Mandantenregel
|
||
über einen Join auf eine solche Tabelle bezieht). Muss in Etappe 2 auf
|
||
`forTenant()` umgestellt werden.
|
||
- **`bewusst-uebergreifend`** — muss über Mandanten hinweg sehen, mit
|
||
ausgeschriebenem Grund. Braucht in Etappe 3 eine sichtbare Kennzeichnung
|
||
(Systemkontext), aber keinen `forTenant()`-Umbau.
|
||
- **`keine-mandantengebundene-tabelle`** — betrifft eines der acht Modelle
|
||
ohne `tenantId` bzw. eine bewusst plattformweite Tabelle (D-03). Kein
|
||
Umbau nötig.
|
||
- **`beides`** — ein vierter, im Plan ausdrücklich verlangter Sonderfall:
|
||
ein Hintergrunddienst, der zurecht über alle Mandanten hinweg eine Liste
|
||
aufbaut (bewusst übergreifend), aber *innerhalb* der Schleife je Mandant
|
||
binden muss (muss mandantengebunden werden). Beide Anteile sind in
|
||
derselben Datei vorhanden; die Fundstelle bekommt hier eine
|
||
Sammelklassifikation, die Aufteilung auf Zeilenebene steht in der
|
||
Begründung.
|
||
|
||
## Zwei belegte Befunde
|
||
|
||
**`req.tenantPrisma` wird gesetzt, aber nirgends gelesen.**
|
||
`tenant.middleware.ts:44` und `tenant.guard.ts:41` setzen
|
||
`req.tenantPrisma = forTenant(this.prisma, tenantId)`. Eine Volltextsuche
|
||
über `apps/api/src` nach `tenantPrisma` außerhalb dieser beiden Dateien und
|
||
ihrer Tests findet keine lesende Stelle — kein Controller greift darauf zu.
|
||
Die Verdrahtung besteht, wird aber nicht genutzt. Für Etappe 2 ist zu
|
||
entscheiden, ob die Controller künftig darüber gehen (dann bräuchte es keinen
|
||
zweiten `forTenant()`-Aufruf je Service-Methode) oder ob der Weg ersatzlos
|
||
entfällt. Dieser Plan entscheidet das nicht, hält den Befund nur fest.
|
||
|
||
**WINDOWS #19 — nullbares `tenantId` bei `SearchProvider` und
|
||
`TenderRssFeedSource`.** Beide Modelle tragen ein nullbares `tenantId`
|
||
(`SearchProvider` für admin-gepflegte Vorgabe-Suchmaschinen, wobei laut
|
||
05-02-Entscheidung die tatsächlichen Vorgaben als Konstanten und nicht als
|
||
DB-Zeilen mit `tenantId = NULL` geführt werden — die Spalte ist nullbar,
|
||
ob es heute tatsächlich `NULL`-Zeilen gibt, ist damit eine offene Frage für
|
||
Etappe 3, nicht eine hier beantwortete; `TenderRssFeedSource` für
|
||
plattformweite RSS-Quellen wie den geseedeten `service.bund.de`-Feed, D-06).
|
||
Die aktuelle Policy `"tenantId" = current_tenant_id()` vergleicht `NULL`
|
||
nie gleich — nach dem Scharfschalten wären plattformweite Zeilen für JEDEN
|
||
Mandanten unsichtbar, nicht nur für fremde. Das ist heute ohne Wirkung
|
||
(Schalter aus, #18), muss aber in Etappe 3 zusammen mit den restlichen
|
||
Fundstellen gelöst werden: die Policy braucht für den Lesezugriff
|
||
`tenantId IS NULL OR tenantId = current_tenant_id()`, während Schreibzugriffe
|
||
weiterhin einen Mandanten verlangen.
|
||
|
||
## Übersicht je Bereich (Zeilentreffer, `this.prisma.*` ohne Specs)
|
||
|
||
Gemessen mit `grep -ro "this\.prisma\.[a-zA-Z]*" apps/api/src/<bereich> | grep -v spec | wc -l`
|
||
am 2026-09-09, **nach** den Änderungen aus Aufgabe 1/2 dieses Plans:
|
||
|
||
| Bereich | Treffer | Hinweis |
|
||
|---|---|---|
|
||
| tenders | 62 | unverändert gegenüber measured_baseline |
|
||
| groups | 37 | unverändert |
|
||
| ldap | 21 | unverändert |
|
||
| dkv | 21 | unverändert |
|
||
| user | 17 | unverändert |
|
||
| module-registry | 17 | unverändert |
|
||
| dashboard | 13 | unverändert |
|
||
| auth | 8 | **war 13 in measured_baseline** — Aufgabe 2 hat 3 Lesezugriffe durch `auth_lookup_*()`-Funktionsaufrufe (`$queryRaw`, kein `this.prisma.<Modell>`) ersetzt und 5 Schreibzugriffe auf `forTenant()`-gebundene Aufrufe (`tenantPrisma.*`, ebenfalls kein `this.prisma.<Modell>`) umgestellt |
|
||
| calendar | 12 | unverändert |
|
||
| tenant | 8 | unverändert |
|
||
| favorites | 7 | unverändert |
|
||
| settings | 4 | unverändert |
|
||
| **Summe** | **227** | war 232 in measured_baseline, Delta = die 5 in Aufgabe 2 verschwundenen `auth`-Treffer minus ein bereits vorher fehlerhaft mitgezähltes Kommentarvorkommen in der neuen Kopfzeile von `validateUser()`, das bewusst umformuliert wurde, um einen Eigentreffer der Bestandsaufnahme-Prüfung zu vermeiden |
|
||
|
||
## Klassen-Verteilung (nach (Datei, Modell)-Fundstellen, 61 Paare)
|
||
|
||
Stand 260909-ipc (Aufgabe 2): 59 Paare aus der urspruenglichen Zaehlung plus
|
||
zwei bisher unentdeckte, weil bereits gebundene Fundstellen
|
||
(`auth.service.ts`/`passwordResetToken`, `ldap.service.ts`/`groupMembership`),
|
||
die erst die um gebundene Zugriffe erweiterte Erkennung (Befund G) sichtbar
|
||
macht — sie waren nie Teil der 227 `this.prisma.*`-Rohtrefferzahl, weil sie
|
||
schon vor diesem Plan über `forTenant()` liefen. Dazu die Korrektur von
|
||
(`ldap-config.service.ts`, `ldapConfig`) von `muss-mandantengebunden` auf
|
||
`beides` (Befund B).
|
||
|
||
| Klasse | Anzahl Paare |
|
||
|---|---|
|
||
| muss-mandantengebunden | 32 |
|
||
| keine-mandantengebundene-tabelle | 16 |
|
||
| beides | 10 |
|
||
| bewusst-uebergreifend | 3 |
|
||
| **Summe** | **61** |
|
||
|
||
## Der Hintergrunddienst als Falle — drei `beides`-Fälle
|
||
|
||
Ein Planer, der über alle Mandanten iteriert, liest zu Recht übergreifend —
|
||
muss aber *innerhalb* der Schleife je Mandant binden. Drei Dateien sind
|
||
betroffen:
|
||
|
||
- **`ldap.service.ts`** (AD-Abgleich): iteriert nicht selbst über alle
|
||
Mandanten (der Sync läuft je Aufruf für einen übergebenen Mandanten), aber
|
||
innerhalb der Sync-Methoden bleiben Lesezugriffe auf `group`, `ldapConfig`
|
||
und `user` teils ungebunden, obwohl der Mandant zu diesem Zeitpunkt bereits
|
||
bekannt ist — die 4 echten `forTenant()`-Aufrufstellen (Zeilen 762, 905,
|
||
1179, 1342) decken nur einen Teil der Lese-/Schreibpfade ab. Das ist der im
|
||
Plankontext benannte Kern von WINDOWS #20: genau dieser Löschzweig
|
||
(~Zeile 1559) deutet Leere nach dem Scharfschalten als "Gruppe im
|
||
Verzeichnis verschwunden".
|
||
- **`tender-digest.scheduler.ts`** (Ausschreibungs-Digest): liest
|
||
`tenderMatch`/`tenderNotificationPref`/`user` bewusst über ALLE Mandanten
|
||
in einem `findMany` (ein einziger globaler Cron-Job, kein Mandant im
|
||
Job selbst — so von Anfang an entworfen, Pitfall 1 in den Kommentaren der
|
||
Datei). Innerhalb der Verteilung je Treffer ist der Mandant aus der Zeile
|
||
bekannt und der Versand muss darauf gebunden laufen.
|
||
- **`tender-matching.service.ts`** (Ausschreibungs-Sofortmeldung): dieselbe
|
||
Form — `tenderMatch`/`tenderSavedSearch`/`user` werden für die
|
||
Sofort-Benachrichtigung über alle betroffenen Mandanten hinweg gelesen,
|
||
der Versand je Treffer ist mandantengebunden.
|
||
|
||
## Bestandsaufnahme
|
||
|
||
Maschinell ermittelt, `rls-access-inventory.spec.ts` hält Vollständigkeit
|
||
nach. Spalten: Datei, Modell (Prisma-Modellname wie in `this.prisma.<Modell>`
|
||
oder — seit 260909-ipc, Befund G — in `<gebundener Client>.<Modell>`
|
||
verwendet), Klasse, Stand (`gebunden`/`ungebunden`/`gemischt`, seit
|
||
260909-ipc maschinell gegen den Quelltext geprüft), Begründung.
|
||
|
||
| Datei | Modell | Klasse | Stand | Begründung |
|
||
|---|---|---|---|---|
|
||
| apps/api/src/auth/auth.service.ts | passwordResetToken | muss-mandantengebunden | gebunden | Kein eigenes `tenantId`, RLS ueber Join auf `User` (Migration 20260618112133). `requestPasswordReset`/`resetPassword` laufen vollstaendig ueber `forTenant()` (Etappe 1, WINDOWS #20, Aufgabe 1) — von der alten, nur `this.prisma.*` erkennenden Suche nie erfasst, weil bereits gebunden; die erweiterte Erkennung aus Aufgabe 2 (260909-ipc) macht diese Fundstelle erstmals sichtbar. |
|
||
| apps/api/src/auth/auth.service.ts | user | muss-mandantengebunden | gemischt | `getMe`, `changePassword`, `adminResetPassword` suchen über die Benutzerkennung aus dem Sitzungsnachweis — der Mandant ist dort bereits bekannt (Aufgabe 2 fasst sie bewusst nicht an, siehe SUMMARY). |
|
||
| apps/api/src/calendar/calendar.service.ts | calendarSource | muss-mandantengebunden | ungebunden | Kalenderquellen eines Nutzers, `tenantId`-Spalte vorhanden. |
|
||
| apps/api/src/dashboard/dashboard.service.ts | dashboardLayout | muss-mandantengebunden | ungebunden | Widget-Anordnung eines Nutzers, `tenantId`-Spalte vorhanden. |
|
||
| apps/api/src/dashboard/dashboard.service.ts | module | keine-mandantengebundene-tabelle | ungebunden | Modulkatalog ist plattformweit, kein `tenantId` (Migration 20260909140000, Gruppe b). |
|
||
| apps/api/src/dashboard/dashboard.service.ts | searchProvider | muss-mandantengebunden | ungebunden | `tenantId` nullbar (WINDOWS #19) — heutige, tatsächlich gespeicherte Zeilen sind nutzerangelegt und tragen einen Mandanten; Vorgabe-Anbieter kommen laut 05-02 aus Konstanten, nicht aus der DB. |
|
||
| apps/api/src/dashboard/dashboard.service.ts | widgetInstance | muss-mandantengebunden | ungebunden | Platzierte Dashboard-Widgets eines Nutzers, `tenantId`-Spalte vorhanden. |
|
||
| apps/api/src/dkv/dkv.service.ts | dkvInvoiceHistory | muss-mandantengebunden | ungebunden | DKV-Rechnungshistorie je Mandant, `tenantId`-Spalte vorhanden. |
|
||
| apps/api/src/dkv/dkv.service.ts | dkvModuleConfig | muss-mandantengebunden | ungebunden | Postfach-/Zugangsdaten des DKV-Moduls je Mandant. |
|
||
| apps/api/src/dkv/dkv.service.ts | dkvVehicleMaster | muss-mandantengebunden | ungebunden | Fahrzeugstammdaten des DKV-Moduls je Mandant. |
|
||
| apps/api/src/favorites/favorites.service.ts | favoriteLink | muss-mandantengebunden | ungebunden | Favoriten-Links eines Nutzers, `tenantId`-Spalte vorhanden. |
|
||
| apps/api/src/groups/groups.service.ts | group | muss-mandantengebunden | ungebunden | Gruppen sind je Mandant, `tenantId`-Spalte vorhanden. |
|
||
| apps/api/src/groups/groups.service.ts | groupMembership | muss-mandantengebunden | ungebunden | Kein eigenes `tenantId`, RLS über Join auf `Group` (Migration 20260618112133-Nachfolger) — braucht trotzdem `forTenant()`, damit der Join-Kontext gesetzt ist. |
|
||
| apps/api/src/groups/groups.service.ts | moduleGrant | muss-mandantengebunden | ungebunden | Modulfreigaben je Mandant, `tenantId`-Spalte vorhanden. |
|
||
| apps/api/src/groups/groups.service.ts | user | muss-mandantengebunden | ungebunden | Nutzerverwaltung innerhalb eines Mandanten. |
|
||
| apps/api/src/groups/module-grants.service.ts | group | muss-mandantengebunden | ungebunden | Wie groups.service.ts. |
|
||
| apps/api/src/groups/module-grants.service.ts | groupMembership | muss-mandantengebunden | ungebunden | Kein eigenes `tenantId`, RLS über Join auf `Group`. |
|
||
| apps/api/src/groups/module-grants.service.ts | moduleGrant | muss-mandantengebunden | ungebunden | Modulfreigaben je Mandant. |
|
||
| apps/api/src/groups/module-grants.service.ts | tenantModuleActivation | muss-mandantengebunden | ungebunden | Welche Module ein Mandant aktiviert hat, `tenantId`-Spalte vorhanden. |
|
||
| apps/api/src/groups/module-grants.service.ts | user | muss-mandantengebunden | ungebunden | Zielbenutzer eines Grants innerhalb des Mandanten. |
|
||
| apps/api/src/ldap/ldap-config.service.ts | ldapConfig | beides | gemischt | `getConfig`, `createConfig` und `updateConfig` laufen ueber `forTenant()`, gebunden an den aus der Anfrage bereits bekannten Mandanten. `getAllActiveConfigs()` (Planer-Lesezugriff ueber ALLE Mandanten) und die Start-Nachverschluesselung in `onApplicationBootstrap` bleiben bewusst uebergreifend: beide laufen, bevor bzw. unabhaengig davon, ob ein einzelner Mandantenkontext feststeht (Befund B, 260909-ipc-PLAN.md). Korrektur der Klasse von `muss-mandantengebunden`: das Paar ist tatsaechlich `beides`, keine Verhaltensaenderung. |
|
||
| apps/api/src/ldap/ldap-config.service.ts | ldapFieldMapping | muss-mandantengebunden | gebunden | Kein eigenes `tenantId`, RLS ueber Join auf `LdapConfig`. `addFieldMapping` und `removeFieldMapping` nehmen den Mandanten jetzt als Parameter entgegen und laufen vollstaendig ueber `forTenant()` (T-IPC-01, 260909-ipc-PLAN.md) — schliesst zugleich die Fremdzugriffsluecke beim Loeschen einer Feldzuordnung ueber ihre Kennung. |
|
||
| apps/api/src/ldap/ldap.service.ts | group | beides | gemischt | AD-Abgleich: 4 echte `forTenant()`-Aufrufstellen decken einen Teil ab, weitere `this.prisma.group`-Zugriffe innerhalb der Sync-Methoden bleiben ungebunden, obwohl der Mandant zu diesem Zeitpunkt bekannt ist (siehe Abschnitt "Der Hintergrunddienst als Falle"). |
|
||
| apps/api/src/ldap/ldap.service.ts | groupMembership | muss-mandantengebunden | gebunden | Kein eigenes `tenantId`, RLS ueber Join auf `Group`. `syncGroupMembershipsForTenant` laeuft vollstaendig ueber `forTenant()` (Plan 16-03/16-05) — von der alten, nur `this.prisma.*` erkennenden Suche nie erfasst, weil bereits gebunden; die erweiterte Erkennung aus Aufgabe 2 (260909-ipc) macht diese Fundstelle erstmals sichtbar. |
|
||
| apps/api/src/ldap/ldap.service.ts | ldapConfig | beides | ungebunden | Dieselbe Begründung wie `group` — Konfigurationszugriffe innerhalb der Sync-Methoden. |
|
||
| apps/api/src/ldap/ldap.service.ts | user | beides | gemischt | Dieselbe Begründung — der Löschzweig um Zeile 1559 (WINDOWS #20) ist der konkrete Risikofall. |
|
||
| apps/api/src/module-registry/module-access.service.ts | module | keine-mandantengebundene-tabelle | ungebunden | Modulkatalog ist plattformweit, kein `tenantId`. |
|
||
| apps/api/src/module-registry/module-access.service.ts | moduleGrant | muss-mandantengebunden | ungebunden | Modulfreigaben je Mandant. |
|
||
| apps/api/src/module-registry/module-access.service.ts | tenantModuleActivation | muss-mandantengebunden | ungebunden | Aktivierung je Mandant, `tenantId`-Spalte vorhanden. |
|
||
| apps/api/src/module-registry/module-registry.service.ts | module | keine-mandantengebundene-tabelle | ungebunden | Modulkatalog ist plattformweit. |
|
||
| apps/api/src/module-registry/module-registry.service.ts | tenantModuleActivation | muss-mandantengebunden | ungebunden | Aktivierung je Mandant. |
|
||
| apps/api/src/settings/settings.service.ts | smtpConfig | muss-mandantengebunden | ungebunden | SMTP-Zugangsdaten je Mandant, `tenantId`-Spalte vorhanden. |
|
||
| apps/api/src/tenant/tenant.controller.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | `Tenant` ist die Mandantentabelle selbst — hat keine eigene `tenantId`-Spalte, kann sie per Definition nicht haben (Migration 20260909140000, Gruppe b). |
|
||
| apps/api/src/tenant/tenant.service.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | Dieselbe Begründung. |
|
||
| apps/api/src/tenders/adapters/email-alert.adapter.ts | tenderEmailConfig | bewusst-uebergreifend | ungebunden | `fetchTenders()` liest bewusst jede aktive `TenderEmailConfig`-Zeile über ALLE Mandanten in einer Abfrage (Plattform-Scheduler, ein Tick pro Postfach, D-13/D-01) — ausführlich im Dateikopf begründet, darf laut Kommentar niemals in `forTenant()` verpackt werden. |
|
||
| apps/api/src/tenders/adapters/rss.adapter.ts | tenderRssFeedSource | bewusst-uebergreifend | ungebunden | Fan-out über jeden aktiven Feed, plattformweit UND persönlich, in einer Abfrage (Zeilen 55–83 im Dateikopf begründet) — dieselbe Scheduler-Ebene wie beim E-Mail-Adapter. |
|
||
| apps/api/src/tenders/tender-dedup.service.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Explizit im Dateikopf: "platform-global, RLS-exempt tables. Never wrap these queries in forTenant()." (D-03) |
|
||
| apps/api/src/tenders/tender-dedup.service.ts | tenderSource | keine-mandantengebundene-tabelle | ungebunden | Dieselbe Begründung. |
|
||
| apps/api/src/tenders/tender-digest.scheduler.ts | tenderMatch | beides | ungebunden | Ein einziger globaler Cron-Job liest über ALLE Mandanten (bewusst übergreifend, Pitfall-1-Kommentar im Dateikopf), der Versand je Treffer ist an dessen Mandanten gebunden. |
|
||
| apps/api/src/tenders/tender-digest.scheduler.ts | tenderNotificationPref | beides | ungebunden | Dieselbe Begründung — Präferenzen werden über alle Mandanten gelesen, aber je Zeile mandantenbezogen ausgewertet. |
|
||
| apps/api/src/tenders/tender-digest.scheduler.ts | user | beides | ungebunden | E-Mail-Adressen für den Versand werden über alle Mandanten gelesen, der eigentliche Versand ist je Treffer mandantengebunden. |
|
||
| apps/api/src/tenders/tender-email-config.service.ts | tenderEmailConfig | muss-mandantengebunden | ungebunden | Nutzer-CRUD für die eigene Postfachanbindung (Phase 17, D-01) — anders als der Fan-out-Adapter oben, hier ist der Mandant aus der Anfrage bekannt. |
|
||
| apps/api/src/tenders/tender-fingerprint-backfill.service.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Einmaliges Backfill-Skript über den plattformweiten `Tender`-Katalog (D-03). |
|
||
| apps/api/src/tenders/tender-ingestion.service.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Explizit im Dateikopf: "Multi-tenant safety (D-03, T-10-09): uses the plain, non-tenant-scoped ... queries ... these are platform-global". |
|
||
| apps/api/src/tenders/tender-ingestion.service.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Plattformweiter Poll-Status, kein `tenantId` (Migration 20260909140000, Gruppe b). |
|
||
| apps/api/src/tenders/tender-matching.service.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Liest den plattformweiten Katalog (D-03), um Treffer zu berechnen — kein `tenantId`. |
|
||
| apps/api/src/tenders/tender-matching.service.ts | tenderMatch | beides | ungebunden | Sofortmeldung: Treffer über alle betroffenen Mandanten gelesen, Versand je Treffer mandantengebunden — siehe Abschnitt "Der Hintergrunddienst als Falle". |
|
||
| apps/api/src/tenders/tender-matching.service.ts | tenderSavedSearch | beides | ungebunden | Dieselbe Begründung — gespeicherte Suchprofile aller Mandanten werden gegen neue Treffer geprüft, der Versand ist je Profil mandantengebunden. |
|
||
| apps/api/src/tenders/tender-matching.service.ts | user | beides | ungebunden | Dieselbe Begründung — E-Mail-Adressen für die Sofortmeldung. |
|
||
| apps/api/src/tenders/tender-notification-pref.service.ts | tenderNotificationPref | muss-mandantengebunden | ungebunden | Nutzer-CRUD für die eigenen Benachrichtigungseinstellungen — Mandant aus der Anfrage bekannt. |
|
||
| apps/api/src/tenders/tender-rss-feed.service.ts | tenderRssFeedSource | muss-mandantengebunden | ungebunden | Nutzer-CRUD für die eigenen RSS-Quellen (anders als der Fan-out in `adapters/rss.adapter.ts`) — Mandant aus der Anfrage bekannt. WINDOWS #19 betrifft die nullbaren plattformweiten Zeilen, nicht diesen CRUD-Pfad. |
|
||
| apps/api/src/tenders/tender-saved-search.service.ts | tenderSavedSearch | muss-mandantengebunden | ungebunden | Nutzer-CRUD für die eigenen gespeicherten Suchprofile — Mandant aus der Anfrage bekannt. |
|
||
| apps/api/src/tenders/tender-scheduler.service.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Plattformweiter DÖE-Poll-Status, kein `tenantId` — im Dateikopf explizit als "genuine platform-wide singleton" begründet. |
|
||
| apps/api/src/tenders/tender-triage.service.ts | tenderTriage | muss-mandantengebunden | ungebunden | Favorisierungs-/Ablehnungsstatus eines Nutzers, `tenantId`-Spalte vorhanden. |
|
||
| apps/api/src/tenders/tenders.controller.ts | tender | keine-mandantengebundene-tabelle | ungebunden | Lesezugriff auf den plattformweiten Katalog (D-03). |
|
||
| apps/api/src/tenders/tenders.controller.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Plattformweiter Poll-Status, admin-verwaltet, kein `tenantId`. |
|
||
| apps/api/src/tenders/tenders.module.ts | tenderSourcePollConfig | keine-mandantengebundene-tabelle | ungebunden | Singleton-Bestückung beim Boot — im Dateikopf explizit als "global, RLS-exempt (D-03)" begründet. |
|
||
| apps/api/src/user/admin-seed.service.ts | tenant | keine-mandantengebundene-tabelle | ungebunden | Legt beim ersten Start den Standard-Mandanten selbst an — `Tenant` hat keine `tenantId`-Spalte. |
|
||
| apps/api/src/user/admin-seed.service.ts | user | bewusst-uebergreifend | ungebunden | Erstanlage des Administrators beim ersten Start: läuft einmalig beim Boot, BEVOR irgendein Mandantenkontext existiert, um den allerersten Mandanten samt Admin-Nutzer anzulegen — es gibt zu diesem Zeitpunkt strukturell keinen Mandanten, an den gebunden werden könnte. |
|
||
| apps/api/src/user/user.controller.ts | user | muss-mandantengebunden | ungebunden | Nutzerverwaltung innerhalb des Mandanten des anfragenden Admins. |
|
||
| apps/api/src/user/user.service.ts | user | muss-mandantengebunden | ungebunden | Dieselbe Begründung. |
|
||
|
||
## Was diese Etappe NICHT entscheidet
|
||
|
||
- Ob Controller künftig über `req.tenantPrisma` statt eines erneuten
|
||
`forTenant()`-Aufrufs im Service gehen (offener Befund oben).
|
||
- Wie die WINDOWS-#19-Policy für `SearchProvider`/`TenderRssFeedSource`
|
||
am Ende genau lautet — nur, dass sie vor dem Scharfschalten gelöst sein
|
||
muss.
|
||
- Die Reihenfolge und Zuschnitt der Etappe-2-Pläne — dafür ist die
|
||
Klassen-Verteilung oben der Arbeitsvorrat, siehe `<next_stages>` im
|
||
Plan `260909-eor-PLAN.md`.
|