Files
tessera-ctl/.planning/phases/17-eigene-ausschreibungs-quellen-je-nutzer/17-01-PLAN.md
T
2026-08-12 08:26:00 +02:00

24 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, user_setup, estimate, must_haves
phase plan type wave depends_on files_modified autonomous requirements user_setup estimate must_haves
17-eigene-ausschreibungs-quellen-je-nutzer 01 execute 1
apps/api/prisma/schema.prisma
apps/api/prisma/migrations/20260812100000_tender_email_config_per_user/migration.sql
apps/api/src/tenders/tender-email-config.service.ts
apps/api/src/tenders/tenders.controller.ts
apps/api/src/tenders/adapters/email-alert.adapter.ts
apps/api/src/tenders/tender-email-config.service.spec.ts
apps/api/src/tenders/adapters/email-alert.adapter.spec.ts
apps/api/src/tenders/email-config-migration-sql.spec.ts
apps/web/src/app/(portal)/modules/tender-radar/my-sources/page.tsx
false
SRC-01
SRC-03
tokens raw_tokens tasks confidence
60000 60000 3 low
truths artifacts key_links
Zwei verschiedene Nutzer desselben Mandanten koennen gleichzeitig je ein eigenes Alert-Postfach hinterlegen (D-01).
Beim Abruf werden beide Postfaecher in einem Durchlauf geleert; faellt eines aus, laufen die anderen weiter (D-01, offener Punkt 3).
Was aus dem Postfach eines Nutzers hereinkommt, bleibt weiterhin auf dessen Mandanten begrenzt — die in Phase 14 gebaute Herkunftsmarkierung bleibt unveraendert (D-05).
Eine bereits vorhandene Postfach-Zeile geht bei der Umstellung nicht verloren, sondern bekommt einen Besitzer (offener Punkt 1).
Die Seite `/modules/tender-radar/my-sources` ist fuer jeden Nutzer mit Modulzugang erreichbar und zeigt sein eigenes Postfach.
apps/api/prisma/migrations/20260812100000_tender_email_config_per_user/migration.sql
apps/api/src/tenders/email-config-migration-sql.spec.ts
apps/web/src/app/(portal)/modules/tender-radar/my-sources/page.tsx
TenderEmailConfig.userId <-> extractTriageContext(req).userId — Besitz kommt ausschliesslich aus dem angemeldeten Konto, nie aus dem Request-Body (IDOR).
TenderEmailConfig.tenantId <-> EmailAlertAdapter.extractCandidates(..., cfg.tenantId, ...) — die Herkunftsmarkierung der eingelesenen Ausschreibungen haengt weiter an diesem Feld.
GET/PUT /modules/tender-radar/email-config <-> Reihenfolge vor `@Get(':id')` — statische Route darf nicht von der Parameter-Route verdeckt werden.
Das Alert-Postfach wechselt vom Mandanten zum einzelnen Nutzer. Heute gibt es genau ein Postfach pro Mandant (`TenderEmailConfig.tenantId @unique`) — ein zweiter Kollege mit eigenem Portal-Konto kann seine Ausschreibungs-Alarme schlicht nicht anbinden. Nach diesem Plan hinterlegt jeder Nutzer sein eigenes Postfach, und der Abruf holt alle hinterlegten Postfaecher in einem Durchlauf ab.

Dieser Plan ist die duenne, aber vollstaendige Bahn durch alle Schichten: Datenbank -> Migration -> Dienst -> Endpunkt -> eine eigene, fuer jeden Modulnutzer erreichbare Seite. Erst wenn diese eine Bahn nachweislich laeuft, bauen die Plaene 17-02 (RSS-Feeds) und 17-03 (Oberflaeche aufteilen) daneben weiter.

Purpose: D-01 aus 17-CONTEXT.md umsetzen und damit die eigentliche Ursache des Backlog-Punkts beheben — nicht nur die Anzeige, sondern den Zuschnitt. Output: Geaenderte Tabelle samt Umzug der Bestandsdaten, nutzerbezogener Dienst und Endpunkt, neue Seite "Meine Quellen" mit dem Postfach-Formular.

Harte Grenze (D-05): Ausschreibungsdaten bleiben plattform-global. Tender bekommt kein Mandantenfeld, keine RLS, keinen Mandanten-Wrapper. Geaendert wird ausschliesslich, wer Quellen einspeist — nicht, wer Treffer sieht. Innerhalb eines Mandanten sieht weiterhin jeder alles.

Entscheidung zum offenen Punkt 1 (Besitzer der Bestandszeile): Eine vorhandene Postfach-Zeile wird dem aeltesten aktiven ADMIN bzw. SUPER_ADMIN ihres Mandanten zugeordnet, nicht geloescht. Begruendung: Die Zeile wurde seinerzeit von genau dieser Rolle angelegt, sie enthaelt verschluesselte Zugangsdaten, und ein Loeschen wuerde den laufenden Abruf ohne Vorwarnung stilllegen. Nur falls ein Mandant ueberhaupt keinen aktiven Administrator hat, wird die Zeile entfernt — dann gaebe es niemanden, der sie je wieder bearbeiten koennte, waehrend das Postfach im Hintergrund weiter abgefragt wuerde.

Entscheidung zum offenen Punkt 4 (wohin mit den nutzereigenen Abschnitten): Es entsteht eine eigene nutzerseitige Modulseite /modules/tender-radar/my-sources ("Meine Quellen"), getrennt von der Administrationsseite. Begruendung: Die Alternative, alles nach /settings/general zu verschieben, sammelt dort mit jedem neuen Modul modulfremde Einstellungen an — genau der Einwand aus dem Backlog-Punkt. Modulsachen bleiben beim Modul; DKV-Fleet und kuenftige Module koennen dieselbe Bauform uebernehmen. Der Adresspfad bleibt englisch (my-sources), wie jeder andere Pfad im Projekt auch; die Beschriftung ist deutsch.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/17-eigene-ausschreibungs-quellen-je-nutzer/17-CONTEXT.md

@apps/api/prisma/schema.prisma @apps/api/src/tenders/tender-email-config.service.ts @apps/api/src/tenders/adapters/email-alert.adapter.ts @apps/api/prisma/migrations/20260811140000_encrypt_ldap_bind_password/migration.sql @apps/api/src/tenders/doe-url-migration-sql.spec.ts

Task 1: Freigabe fuer die beiden Datenbank-Umbauten dieser Phase Darf ich die Tabellenstruktur fuer Postfaecher und RSS-Feeds jetzt umbauen? Diese Phase aendert zwei Tabellen dauerhaft. Beides laesst sich nur mit einer weiteren Datenbank-Aenderung zurueckdrehen, nicht durch einfaches Rueckgaengigmachen der Dateien:
  1. Postfaecher (dieser Plan): Das Postfach haengt danach am Nutzer statt am Mandanten. Eine bereits eingerichtete Postfach-Zeile bekommt dabei den aeltesten aktiven Administrator ihres Mandanten als Besitzer. Findet sich dort kein Administrator, wird die Zeile entfernt — sie waere sonst fuer niemanden mehr bearbeitbar, wuerde aber weiter abgefragt.
  2. RSS-Feeds (Plan 17-02): Die Feeds bekommen ein Besitzer-Feld. Alle heute vorhandenen Feeds bleiben unveraendert plattformweit fuer alle aktiv — dort geht nichts verloren.

Auf dem Testserver liegen echte Daten. Der Umbau selbst laeuft hier lokal; auf dem Testserver wird er erst wirksam, wenn Sie dort selbst neu ausrollen. Vor diesem Ausrollen ist eine Datenbank-Sicherung sinnvoll.

Bevor ich anfange, zaehle ich nach, wie viele Postfach-Zeilen es auf dem Testserver ueberhaupt gibt, und nenne Ihnen die Zahl — nur lesend, ich fasse dort nichts an. Weiter — Umbau durchfuehren Die Phase kann wie besprochen gebaut werden; jeder bekommt sein eigenes Postfach. Der Schritt ist nur mit einer weiteren Aenderung rueckdrehbar. Stopp — erst Sicherung, dann neu ansetzen Kein Risiko, bis eine Sicherung vorliegt. Die Phase pausiert. Antworten Sie mit "weiter" oder "stopp".

Task 2: Ein eigenes Postfach je Nutzer — durchgehend von der Datenbank bis zur Seite Die lokale Entwicklungsdatenbank ist ohne Host-Port erreichbar: Container-IP des `db`-Containers ermitteln (`docker inspect`) und `DATABASE_URL` mit Zugang `tessera:tessera_dev` darauf zeigen lassen — sonst laesst sich die Migration nicht anwenden (Projektwissen "Lokale DB-Migrationen"). Die Migration schreibt Bestandsdaten um und entfernt eine Eindeutigkeitsregel; ein Rueckweg braucht eine zweite Migration. Freigabe erfolgt in Task 1. apps/api/prisma/schema.prisma, apps/api/prisma/migrations/20260812100000_tender_email_config_per_user/migration.sql, apps/api/src/tenders/tender-email-config.service.ts, apps/api/src/tenders/tenders.controller.ts, apps/web/src/app/(portal)/modules/tender-radar/my-sources/page.tsx apps/api/src/tenders/tender-email-config.service.ts, apps/api/src/tenders/tenders.controller.ts (Zeilen 90-115 und 270-305), apps/api/prisma/migrations/20260723113917_tender_email_config_owner_tenant_id/migration.sql, apps/web/src/app/(portal)/modules/tender-radar/settings/page.tsx - Ein Nutzer speichert sein Postfach; danach liest derselbe Nutzer genau diese Werte zurueck. - Ein zweiter Nutzer desselben Mandanten speichert ein anderes Postfach; beide Zeilen existieren nebeneinander, keine ueberschreibt die andere. - Nutzer B liest nie die Werte von Nutzer A — gelesen wird ausschliesslich nach dem angemeldeten Konto. - Das Passwort verlaesst den Server nie: die Antwort enthaelt weiterhin nur `hasPassword: boolean`. - Wird nur der Benutzername geaendert und das Passwortfeld leer gelassen, bleibt das gespeicherte Passwort erhalten (bestehende Semantik, unveraendert). **Vorabpruefung (nur lesend, Testserver).** Ueber SSH auf 192.168.13.12 mit `psql` zaehlen, wie viele Zeilen in `TenderEmailConfig` stehen und zu welchem Mandanten sie gehoeren. Ergebnis im SUMMARY festhalten. Nichts aendern, kein `docker compose`-Kommando dort ausfuehren — Ausrollen ist Sache des Nutzers.

Schema (apps/api/prisma/schema.prisma, Modell TenderEmailConfig). Feld userId String @unique ergaenzen. tenantId von @unique auf ein gewoehnliches Feld zurueckstufen (bleibt erhalten, denormalisiert, fuer die SMTP-Aufloesung und die Herkunftsmarkierung — gleiche Rolle wie in TenderMatch, D-01). Bestehenden @@index([tenantId]) behalten, @@index([userId]) ergaenzen. Den Kopfkommentar des Modells um zwei Saetze erweitern: Besitz liegt beim Nutzer (D-01, Phase 17), tenantId bleibt denormalisiert.

Migration. Verzeichnis apps/api/prisma/migrations/20260812100000_tender_email_config_per_user/ anlegen, migration.sql von Hand schreiben (Projektentscheidung "Migrationsverfahren angepasst": prisma migrate dev verweigert die nicht-interaktive Shell). Erwarteter Ablauf, in dieser Reihenfolge:

  1. Spalte userId als TEXT ohne Pflicht ergaenzen.
  2. Bestandszeilen befuellen: je Zeile den aeltesten aktiven Nutzer mit Rolle ADMIN oder SUPER_ADMIN desselben Mandanten ermitteln (sortiert nach createdAt, bei Gleichstand nach id, LIMIT 1) und dessen id eintragen.
  3. Zeilen, die danach immer noch keinen Besitzer haben, entfernen (kein Administrator im Mandanten vorhanden — siehe Begruendung im Objective).
  4. Eindeutigkeitsregel auf tenantId entfernen (TenderEmailConfig_tenantId_key), gewoehnlichen Index ..._tenantId_idx stehen lassen.
  5. Spalte userId auf Pflicht setzen, eindeutigen Index TenderEmailConfig_userId_key und Index TenderEmailConfig_userId_idx anlegen.

Ueber die Anweisungen einen deutschen Kommentarkopf setzen, der erklaert, warum umgestellt wird und warum die Bestandszeile dem aeltesten Administrator zufaellt — gleiche Form wie 20260811140000_encrypt_ldap_bind_password/migration.sql.

Anwenden mit prisma migrate deploy gegen die lokale Datenbank, danach prisma generate.

Dienst (tender-email-config.service.ts). getConfigForApi nimmt statt tenantId nun userId und sucht darueber. saveConfig nimmt ein Objekt { userId, tenantId } als ersten Parameter; im upsert ist userId der Schluessel, tenantId wird sowohl beim Anlegen als auch beim Aktualisieren mitgeschrieben (der Mandant eines Nutzers kann sich aendern, das denormalisierte Feld muss mitziehen). EMAIL_CONFIG_SAFE_SELECT um userId ergaenzen; der verschluesselte Zugangsdaten-Block bleibt wie bisher aus jeder Antwort ausgeschlossen. Die Semantik "Passwort leer heisst gespeichertes Passwort behalten" bleibt unveraendert; nur die Suchbedingungen wechseln von tenantId auf userId. Klassenkommentar entsprechend nachziehen.

Endpunkt (tenders.controller.ts). GET/PUT /email-config: die Rollenpruefung @Roles(ADMIN, SUPER_ADMIN) entfaellt und wird durch @UseModule('tender-radar') ersetzt — das Postfach ist ab jetzt eine Nutzereinstellung, der Zugang haengt an der Modulfreigabe aus Phase 15. userId und tenantId kommen weiterhin ausschliesslich aus extractTriageContext(req), nie aus dem Rumpf der Anfrage. Die Position beider Handler im Datei-Aufbau nicht verschieben: sie stehen vor der Parameter-Route @Get(':id'), sonst verdeckt diese sie und die Endpunkte antworten mit 404 (dokumentierte Falle, Unit-Tests fangen sie nicht). Die vorhandenen Kommentare zu dieser Reihenfolge stehen lassen und den Hinweis auf die Mandantenbindung im Kommentar auf die Nutzerbindung umschreiben.

Seite (apps/web/src/app/(portal)/modules/tender-radar/my-sources/page.tsx). Neue Client-Seite, gleiche Bauform wie die vorhandene Einstellungsseite: Ueberschrift aus dem i18n-Namensraum tenderRadar (neuer Schluessel mySources.title, deutsch "Meine Quellen", englisch "My sources"; beide Sprachdateien apps/web/src/messages/de.json und en.json ergaenzen), darunter ein Abschnitt "Mein Postfach" mit dem unveraenderten bestehenden EmailAlertConfigForm (nur importieren, nicht kopieren, nicht umbauen — es spricht dieselben Endpunkte an). Ein kurzer Hinweistext unter der Ueberschrift benennt ehrlich, was D-05 bedeutet: die hier eingespeisten Ausschreibungen erscheinen in der Trefferliste aller Kollegen desselben Mandanten. Keine Aenderung am Modul-Verzeichnis noetig — verschachtelte Route unterhalb der bereits freigeschalteten Modulseite, gleiche Begruendung wie bei der Einstellungsseite. cd /home/vicolab/projects/tessera-ctl/apps/api && npx prisma validate && npx prisma migrate status cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/api type-check && pnpm --filter @tessera/web type-check cd /home/vicolab/projects/tessera-ctl/apps/api && npx prisma db execute --stdin <<'SQL' SELECT indexname FROM pg_indexes WHERE tablename = 'TenderEmailConfig' ORDER BY indexname; SQL Im Browser als normaler Nutzer (Rolle USER mit Modulfreigabe) /modules/tender-radar/my-sources oeffnen: die Seite laedt, das Postfach-Formular ist bedienbar, Speichern quittiert erfolgreich und nach einem Neuladen stehen die Werte wieder da. Danach mit einem zweiten Konto desselben Mandanten anmelden: dort steht ein leeres Formular, nicht die Werte des ersten Nutzers. prisma migrate status meldet alle Migrationen angewandt. Die Indexliste zeigt TenderEmailConfig_userId_key und enthaelt TenderEmailConfig_tenantId_key nicht mehr. Beide Typpruefungen laufen fehlerfrei. Zwei Konten desselben Mandanten haben je eine eigene Postfach-Zeile.

Task 3: Der Abruf holt alle Postfaecher — und beweist es mit Tests apps/api/src/tenders/adapters/email-alert.adapter.ts, apps/api/src/tenders/adapters/email-alert.adapter.spec.ts, apps/api/src/tenders/tender-email-config.service.spec.ts, apps/api/src/tenders/email-config-migration-sql.spec.ts apps/api/src/tenders/adapters/email-alert.adapter.spec.ts, apps/api/src/tenders/tender-email-config.service.spec.ts, apps/api/src/tenders/doe-url-migration-sql.spec.ts - Zwei aktive Postfaecher **desselben** Mandanten werden in einem einzigen Abruf beide geleert. Das ist genau die Faehigkeit, die es vorher nicht gab — der Test faellt ohne Task 2 durch. - Faellt eines von zwei Postfaechern mit einem Fehler aus, liefert der Abruf trotzdem die Datensaetze des anderen und protokolliert eine Warnung. - Die eingelesenen Datensaetze tragen weiterhin die Herkunftsmarkierung aus dem Mandantenfeld der jeweiligen Postfach-Zeile (unveraenderte Sichtbarkeit gegenueber anderen Mandanten, D-05). - Die Sammelabfrage liest weiterhin alle aktiven Postfaecher ueber Mandantengrenzen hinweg in einem Zug — der Plattform-Zeitplan ist keine Anfrage eines einzelnen Mandanten. - Die Migrationsdatei ordnet Bestandszeilen einem Administrator zu, bevor sie unbesetzte Zeilen entfernt. **Anpassung im Abruf (`email-alert.adapter.ts`).** Inhaltlich aendert sich nichts an der Mechanik: die Sammelabfrage bleibt exakt wie sie ist (alle aktiven Zeilen in einem Zug), und die Herkunftsmarkierung der Datensaetze kommt weiterhin aus dem Mandantenfeld der jeweiligen Zeile — dieses Feld existiert nach Task 2 unveraendert. Zu aendern sind nur zwei Dinge: (a) die Warnmeldung im Fehlerfall benennt jetzt das Postfach ueber die Zeilen-`id` und die Besitzer-Kennung statt des Mandanten, weil ein Mandant ab sofort mehrere Postfaecher haben kann und die alte Meldung sonst mehrdeutig waere — Zugangsdaten duerfen dabei nach wie vor nirgends im Protokoll landen; (b) der Klassenkommentar erklaert, dass die Auffaecherung ab Phase 17 pro Postfach und nicht mehr pro Mandant laeuft. Die ausdrueckliche Warnung im Kommentar, dass diese Sammelabfrage niemals in einen Mandanten-Wrapper gehoert, bleibt Wort fuer Wort stehen. Die Fehlerbehandlung je Postfach (ein kaputtes Postfach blockiert die anderen nicht) bleibt unangetastet.

Tests email-alert.adapter.spec.ts. Bestehende Faelle auf die neue Datenform nachziehen (Zeilen tragen jetzt zusaetzlich userId) und drei neue Faelle ergaenzen:

  1. Zwei aktive Zeilen mit gleichem Mandanten, aber verschiedenen Besitzern und verschiedenen Postfachdaten: der Postfach-Anbieter wird zweimal aufgerufen, mit den jeweils eigenen Zugangsdaten, und das Ergebnis enthaelt die Kandidaten aus beiden Postfaechern.
  2. Von zwei Zeilen wirft die erste beim Abholen einen Fehler: das Ergebnis enthaelt trotzdem die Datensaetze der zweiten, und eine Warnung wurde protokolliert.
  3. Die Herkunftsmarkierung der erzeugten Datensaetze entspricht dem Mandantenfeld der jeweiligen Zeile — bei zwei Zeilen unterschiedlicher Mandanten also zwei unterschiedliche Werte.

Wichtig: Die Erwartungswerte in diesen Tests von Hand hinschreiben. Sie duerfen nicht ueber dieselbe Hilfsfunktion erzeugt werden, die auch der Produktivcode benutzt — ein Test, der seine Erwartung mit dem Pruefling selbst baut, beweist nichts (dokumentierte Anti-Pattern im Projekt).

Tests tender-email-config.service.spec.ts. Auf den neuen Schluessel umstellen: gelesen und geschrieben wird nach Besitzer. Zwei Faelle ergaenzen: Speichern legt beim Anlegen sowohl Besitzer als auch Mandant an; ein zweiter Nutzer im selben Mandanten erzeugt eine zweite Zeile statt die erste zu ueberschreiben. Der bestehende Fall, dass der verschluesselte Zugangsdatenblock nie in der Antwort auftaucht, bleibt erhalten und muss weiterhin durchlaufen.

Neuer Test email-config-migration-sql.spec.ts. Nach dem Vorbild von doe-url-migration-sql.spec.ts die von Hand geschriebene Migrationsdatei ohne Datenbank pruefen, rein als Text: die Zuordnung zum Administrator kommt in der Datei vor dem Entfernen unbesetzter Zeilen; die Auswahl beschraenkt sich auf aktive Nutzer mit Administratorrolle desselben Mandanten und nimmt genau einen; die alte Eindeutigkeitsregel auf dem Mandantenfeld wird entfernt und eine neue auf dem Besitzerfeld angelegt; die Pflichtsetzung der neuen Spalte steht nach der Befuellung. cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/api exec vitest run src/tenders/adapters/email-alert.adapter.spec.ts src/tenders/tender-email-config.service.spec.ts src/tenders/email-config-migration-sql.spec.ts cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/api exec vitest run src/tenders Alle Tests im Bereich src/tenders laufen gruen. Der Fall "zwei Postfaecher desselben Mandanten werden beide abgeholt" existiert und besteht. Der Fall "ein kaputtes Postfach blockiert die anderen nicht" existiert und besteht. Die Migrationsdatei wird textuell geprueft.

<threat_model>

Trust Boundaries

Boundary Description
Browser -> API (/modules/tender-radar/email-config) Ab sofort erreicht jeder Nutzer mit Modulfreigabe diese Endpunkte, nicht mehr nur Administratoren. Besitzangaben aus der Anfrage sind grundsaetzlich nicht vertrauenswuerdig.
API -> Datenbank (TenderEmailConfig) Traegt verschluesselte Postfach-Zugangsdaten.
Plattform-Zeitplan -> fremdes Postfach Ausgehende Verbindung an eine vom Nutzer angegebene Adresse.

STRIDE Threat Register

Threat ID Category Component Severity Disposition Mitigation Plan
T-17-01 Elevation of Privilege GET/PUT /email-config high mitigate Besitzer und Mandant kommen ausschliesslich aus extractTriageContext(req); die DTO traegt kein Besitzer- und kein Mandantenfeld. Test: ein zweiter Nutzer liest nie die Zeile des ersten.
T-17-02 Information Disclosure Antwort von GET /email-config high mitigate EMAIL_CONFIG_SAFE_SELECT schliesst den verschluesselten Zugangsdatenblock weiterhin aus; die Antwort traegt nur hasPassword. Bestehender Test bleibt aktiv.
T-17-03 Information Disclosure Protokollausgabe im Abruf medium mitigate Die neue Warnmeldung nennt nur Zeilen-id und Besitzer-Kennung, nie Benutzername, Passwort oder Postfachadresse.
T-17-04 Denial of Service Abruf ueber mehrere Postfaecher medium mitigate Fehlerbehandlung je Postfach bleibt erhalten — ein haengendes Postfach blockiert die uebrigen nicht. Test deckt den Fall ab.
T-17-05 Tampering Hand geschriebene Migration high mitigate Reihenfolge (befuellen vor loeschen, Pflicht nach Befuellung) wird textuell getestet; Anwendung zuerst nur lokal, Ausrollen auf den Testserver bleibt Nutzeraktion.
T-17-06 Elevation of Privilege Wegfall der Rollenpruefung an den Postfach-Endpunkten medium accept Beabsichtigt (D-01): das Postfach ist eine Nutzereinstellung. Der Zugang bleibt ueber die Modulfreigabe aus Phase 15 gedeckelt.
T-17-SC Tampering Paketinstallationen low accept Diese Phase installiert kein neues Paket — keine Lieferketten-Pruefung noetig.
</threat_model>
1. `pnpm --filter @tessera/api exec vitest run src/tenders` — vollstaendig gruen. 2. `pnpm --filter @tessera/api type-check` und `pnpm --filter @tessera/web type-check` — fehlerfrei. 3. `npx prisma migrate status` im API-Verzeichnis — keine offene Migration. 4. Indexliste von `TenderEmailConfig` enthaelt einen eindeutigen Index auf dem Besitzerfeld und keinen mehr auf dem Mandantenfeld. 5. Browser-Gegenprobe mit zwei Konten desselben Mandanten (siehe `human-check` in Task 2).

<success_criteria>

  • Zwei Nutzer desselben Mandanten haben je eine eigene Postfach-Zeile.
  • Ein Nutzer sieht ueber die API ausschliesslich sein eigenes Postfach.
  • Der Abruf holt in einem Durchlauf alle aktiven Postfaecher; ein Ausfall blockiert die anderen nicht.
  • Eine vorhandene Postfach-Zeile hat nach der Migration einen Besitzer.
  • /modules/tender-radar/my-sources ist fuer jeden Nutzer mit Modulfreigabe erreichbar und zeigt dessen Postfach.
  • Tender bleibt unveraendert ohne Mandantenfeld und ohne RLS (D-05). </success_criteria>
Create `.planning/phases/17-eigene-ausschreibungs-quellen-je-nutzer/17-01-SUMMARY.md` when done. Im SUMMARY festhalten: Anzahl der auf dem Testserver vorgefundenen Postfach-Zeilen und wem sie zugeordnet wurden.