--- phase: 17-eigene-ausschreibungs-quellen-je-nutzer plan: 01 subsystem: api tags: [prisma, postgresql, nestjs, nextjs, multi-tenancy, migration] # Dependency graph requires: [] provides: - "TenderEmailConfig.userId @unique (Besitz beim Nutzer statt Mandant, D-01)" - "Handgeschriebene Migration mit Bestandsdaten-Umzug (aeltester aktiver Admin uebernimmt, sonst Loeschung)" - "TenderEmailConfigService per-user (getConfigForApi/saveConfig)" - "GET/PUT /modules/tender-radar/email-config @UseModule('tender-radar') statt @Roles(ADMIN,SUPER_ADMIN)" - "/modules/tender-radar/my-sources ('Meine Quellen') — neue nutzerseitige Modulseite" - "EmailAlertAdapter-Fanout jetzt pro Postfach-Zeile statt pro Mandant (Mechanik unveraendert)" affects: [17-02-rss-feed-owner, 17-03-ui-aufteilung] # Actuals (#2632) actuals: tokens: 13600 tasks: 3 commits: 3 tech-stack: added: [] patterns: - "Handgeschriebene Prisma-Migration mit Backfill-Regel (aeltester aktiver Admin/SUPER_ADMIN je Mandant) vor Pflichtsetzung + Eindeutigkeits-Wechsel — gleiche Werkzeugkette wie 16-01/20260811140000 (migrate diff + Handdatei + migrate deploy)" - "Migrations-SQL wird per Text-Spec (kein DB-Zugriff) auf Reihenfolge geprueft, gleiches Muster wie doe-url-migration-sql.spec.ts" key-files: created: - 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" modified: - apps/api/prisma/schema.prisma - 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 key-decisions: - "Checkpoint 1 (17-01 Task 1, gate=blocking): 'weiter' — Nutzer hat beide Datenbank-Umbauten der Phase (Postfach je Nutzer 17-01, RSS-Feed-Besitzer 17-02) freigegeben am 2026-08-12, nachdem die Zaehlung 0 Bestandszeilen sowohl lokal als auch auf dem alpha-Testserver ergab (lesend per SSH gemessen, keine Aenderung dort vorgenommen)" - "tenantId bleibt denormalisiert auf TenderEmailConfig und wird bei saveConfig auf CREATE UND UPDATE neu geschrieben, nicht nur beim Anlegen — der Mandant eines Nutzers kann sich aendern" - "email-config-Routen von @Roles(ADMIN,SUPER_ADMIN) auf @UseModule('tender-radar') umgestellt: das Postfach ist jetzt eine Nutzereinstellung, Zugang haengt an der Modulfreigabe aus Phase 15 (T-17-06, disposition=accept, beabsichtigt laut D-01)" - "Eigene Seite /modules/tender-radar/my-sources statt Erweiterung von /settings/general/account (D-01 offener Punkt 4) — Modul-Einstellungen bleiben beim Modul" - "EmailAlertAdapter-Warnmeldung im Fehlerfall nennt jetzt Zeilen-id + Besitzer-userId statt tenantId (T-17-03) — ein Mandant kann seit dieser Migration mehrere Postfaecher haben, die alte Meldung waere mehrdeutig" requirements-completed: [SRC-01, SRC-03] coverage: - id: D1 description: "Migration verschiebt TenderEmailConfig-Besitz von tenantId auf userId; Bestandszeilen werden dem aeltesten aktiven Admin/SUPER_ADMIN ihres Mandanten zugeordnet, unbesetzte Zeilen entfernt, alte tenantId-Eindeutigkeit faellt, neue userId-Eindeutigkeit entsteht" requirement: "SRC-01" verification: - kind: unit ref: "apps/api/src/tenders/email-config-migration-sql.spec.ts (6 Tests, reiner Textabgleich der Reihenfolge)" status: pass - kind: integration ref: "prisma migrate deploy gegen lokale Dev-DB (172.19.0.2) + prisma migrate status + Index-Abfrage (TenderEmailConfig_userId_key vorhanden, TenderEmailConfig_tenantId_key entfernt, TenderEmailConfig_tenantId_idx bleibt)" status: pass human_judgment: false - id: D2 description: "TenderEmailConfigService liest/schreibt ausschliesslich nach userId; zwei Nutzer desselben Mandanten erhalten zwei unabhaengige Zeilen; Passwort verlaesst den Server nie (nur hasPassword)" requirement: "SRC-01" verification: - kind: unit ref: "apps/api/src/tenders/tender-email-config.service.spec.ts (8 Tests)" status: pass human_judgment: false - id: D3 description: "GET/PUT /modules/tender-radar/email-config sind @UseModule-gated statt @Roles-gated; userId/tenantId kommen ausschliesslich aus dem Auth-Kontext, nie aus Query/Body; zwei verschiedene Nutzer loesen unterschiedliche userIds auf (IDOR-Schutz, T-17-01)" requirement: "SRC-01" verification: - kind: unit ref: "apps/api/src/tenders/tenders.controller.spec.ts (email-config describe block, 3 Tests inkl. neuem IDOR-Test)" status: pass human_judgment: false - id: D4 description: "EmailAlertAdapter holt mehrere aktive Postfaecher DESSELBEN Mandanten in einem Durchlauf ab, mit je eigenen Zugangsdaten; ein kaputtes Postfach blockiert die anderen nicht; Herkunftsmarkierung folgt weiterhin dem tenantId-Feld der jeweiligen Zeile" requirement: "SRC-01" verification: - kind: unit ref: "apps/api/src/tenders/adapters/email-alert.adapter.spec.ts (23 Tests, davon 3 neu fuer Phase 17: Mehrfach-Postfach-Fanout, Katalog-per-Mailbox-Fehlerbehandlung mit Warnmeldung, ownerTenantId-Divergenz)" status: pass human_judgment: false - id: D5 description: "/modules/tender-radar/my-sources ist fuer jeden Nutzer mit Modulzugang erreichbar, zeigt sein eigenes Postfach-Formular (EmailAlertConfigForm unveraendert wiederverwendet) und benennt D-05 (Sichtbarkeit bleibt mandantenweit geteilt) im Hinweistext" requirement: "SRC-03" verification: [] human_judgment: true rationale: "Browser-Gegenprobe mit zwei Konten desselben Mandanten (laut Plan- human-check) nicht ausgefuehrt — kein Browser-Tool in dieser Session verfuegbar. Als WINDOWS.md #7 (unrun-verify) festgehalten. Automatisierte Pruefungen (Typprüfung web+api, Komponente kompiliert, i18n-Keys vorhanden) sind gelaufen und gruen." duration: 76min completed: 2026-08-12 status: complete --- # Phase 17 Plan 01: Eigenes Alert-Postfach je Nutzer Summary **TenderEmailConfig-Besitz per Handmigration von tenantId auf userId umgezogen (Bestandszeilen dem aeltesten aktiven Admin zugeordnet), Service/Controller/Adapter auf Nutzer-Scoping umgestellt, neue Seite `/modules/tender-radar/my-sources` mit dem unveraenderten Postfach-Formular.** ## Performance - **Duration:** 76 min (inkl. Checkpoint-Wartezeit auf Nutzerfreigabe) - **Started:** 2026-08-12T08:08:41Z - **Completed:** 2026-08-12T09:25:00Z - **Tasks:** 3 (Checkpoint-Entscheidung, Tracer-Task durchgehende Bahn, Test-Erweiterung Adapter) - **Files modified:** 12 (3 neu, 9 geaendert) ## Bestandsaufnahme aus dem Checkpoint (Task 1) Vor der Freigabe wurde gezaehlt, wie viele `TenderEmailConfig`-Zeilen tatsaechlich existieren: - **Lokale Entwicklungsdatenbank:** 0 Zeilen - **Testserver alpha.tessera.ctl.de (192.168.13.12):** 0 Zeilen (lesend per SSH/psql gezaehlt, keine Aenderung dort vorgenommen) Die im Plan beschriebene Umzugsregel ("aeltester aktiver Administrator uebernimmt die Bestandszeile, sonst wird sie geloescht") hatte damit **keine echten Daten zu bewegen** — sie ist als Logik in der Migrationsdatei angelegt und per Text-Spec abgesichert, griff aber bei der tatsaechlichen Anwendung ins Leere. Der Nutzer hat auf dieser Grundlage mit "weiter" beide Datenbank-Umbauten der Phase (dieser Plan + Plan 17-02) freigegeben. ## Accomplishments - `TenderEmailConfig.userId @unique` ersetzt `tenantId @unique` — zwei Nutzer desselben Mandanten koennen jetzt gleichzeitig je ein eigenes Alert-Postfach hinterlegen (D-01) - Handgeschriebene Migration mit korrekter Backfill-Reihenfolge (Zuordnung vor Loeschung, Pflicht erst nach Befuellung), lokal angewendet und per Index-Abfrage verifiziert - `TenderEmailConfigService` und die `GET`/`PUT /email-config`-Endpunkte lesen/schreiben ausschliesslich nach `userId` aus dem Auth-Kontext — nie aus Query/Body (IDOR-Schutz) - `EmailAlertAdapter` holt weiterhin alle aktiven Postfaecher mandantenuebergreifend in einem Zug ab (Mechanik unveraendert), jetzt nachweislich auch mehrere Postfaecher desselben Mandanten; ein kaputtes Postfach blockiert die anderen nicht - Neue Seite `/modules/tender-radar/my-sources` ("Meine Quellen") mit dem unveraenderten `EmailAlertConfigForm` und einem Hinweistext, der D-05 ehrlich benennt (eingelesene Ausschreibungen bleiben mandantenweit sichtbar) - `Tender` bleibt unveraendert plattform-global — keine Sichtbarkeitstrennung eingefuehrt (D-05, harte Grenze eingehalten) ## Task Commits Jede Aufgabe wurde einzeln committet: 1. **Task 1: Freigabe fuer die beiden Datenbank-Umbauten dieser Phase** — Checkpoint, kein eigener Commit (Entscheidung "weiter" durch den Nutzer, dokumentiert oben und in STATE.md) 2. **Task 2: Ein eigenes Postfach je Nutzer — durchgehend von der Datenbank bis zur Seite** — `05b1d29` (feat) 3. **Task 3: Der Abruf holt alle Postfaecher — und beweist es mit Tests** — `55ceb24` (test) **Plan metadata:** wird mit diesem SUMMARY committet (docs) ## Files Created/Modified - `apps/api/prisma/schema.prisma` — `TenderEmailConfig.userId @unique` ergaenzt, `tenantId` auf gewoehnliches Feld zurueckgestuft, `@@index([userId])` ergaenzt - `apps/api/prisma/migrations/20260812100000_tender_email_config_per_user/migration.sql` — Handmigration: Spalte anlegen, Bestandszeilen dem aeltesten aktiven Admin zuordnen, unbesetzte Zeilen loeschen, Eindeutigkeit von tenantId auf userId umziehen - `apps/api/src/tenders/email-config-migration-sql.spec.ts` — neuer Text-Spec (kein DB-Zugriff), prueft die Reihenfolge der Handmigration - `apps/api/src/tenders/tender-email-config.service.ts` — `getConfigForApi(userId)`, `saveConfig({userId, tenantId}, dto)`, tenantId auf create UND update mitgeschrieben - `apps/api/src/tenders/tender-email-config.service.spec.ts` — auf userId-Schluessel umgestellt, plus zwei neue Faelle (tenantId beim Anlegen, zwei Nutzer = zwei Zeilen) - `apps/api/src/tenders/tenders.controller.ts` — email-config-Routen von `@Roles(ADMIN,SUPER_ADMIN)` auf `@UseModule('tender-radar')`, `userId`/`tenantId` aus `extractTriageContext(req)`, Route-Reihenfolge vor `@Get(':id')` unveraendert - `apps/api/src/tenders/tenders.controller.spec.ts` — Fake-Service-Signaturen + Tests an userId-Schluessel angepasst (Rule 3, nicht im Plan gelistet), plus neuer IDOR-Test - `apps/api/src/tenders/adapters/email-alert.adapter.ts` — Warnmeldung nennt Zeilen-id + Besitzer statt Mandant (T-17-03); Sammelabfrage-Mechanik und Fehlerbehandlung je Zeile unangetastet - `apps/api/src/tenders/adapters/email-alert.adapter.spec.ts` — Config-Fixtures um `id`/`userId` ergaenzt, drei neue Faelle (Mehrfach-Postfach-Fanout, Warnmeldungs-Inhalt, ownerTenantId-Divergenz) - `apps/web/src/app/(portal)/modules/tender-radar/my-sources/page.tsx` — neue Seite "Meine Quellen" - `apps/web/src/messages/de.json`, `apps/web/src/messages/en.json` — `tenderRadar.mySources.*` Schluessel ergaenzt ## Decisions Made - Checkpoint 1 (gate=blocking): "weiter" — Freigabe fuer beide Datenbank-Umbauten der Phase, gestuetzt auf gemessene 0 Bestandszeilen (lokal und alpha) - tenantId bleibt denormalisiert, wird bei jedem Save (create UND update) neu geschrieben - email-config-Endpunkte wechseln von rollenbasiert auf modulbasiert (D-01 beabsichtigt, T-17-06 disposition=accept) - Eigene Seite statt Erweiterung von `/settings/general/account` (D-01 offener Punkt 4) - Adapter-Warnmeldung nennt Zeilen-id + userId statt tenantId, da ein Mandant jetzt mehrere Postfaecher haben kann ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 3 - Blocking] `tenders.controller.spec.ts` an neue Service-Signatur angepasst** - **Found during:** Task 2 (Type-Check nach Controller-Umstellung) - **Issue:** Die bestehenden Controller-Tests riefen `getConfigForApi(tenantId)`/`saveConfig(tenantId, dto)` mit der alten Signatur auf — nicht im Plan gelistet, aber ohne Anpassung kompiliert das Projekt nicht und die Tests schlagen fehl - **Fix:** Fake-Service-Signaturen und die zwei email-config-Tests auf `getConfigForApi(userId)`/`saveConfig({userId,tenantId}, dto)` umgestellt; einen zusaetzlichen IDOR-Test ergaenzt (zwei Nutzer loesen unterschiedliche userIds auf) - **Files modified:** apps/api/src/tenders/tenders.controller.spec.ts - **Verification:** `pnpm --filter @tessera/api type-check` fehlerfrei, 49/49 Tests in der Datei gruen - **Committed in:** 05b1d29 (Task 2 commit) **2. [Rule 3 - Blocking] `tender-email-config.service.spec.ts` vollstaendig auf userId umgeschrieben, bereits in Task 2** - **Found during:** Task 2 (Type-Check — `saveConfig` erwartet jetzt ein Objekt statt eines Strings) - **Issue:** Die Plan-Zuordnung dieser Datei zu Task 3 haette den Type-Check von Task 2 nicht bestehen lassen — der Compiler kennt keine Task-Grenzen - **Fix:** Datei bereits in Task 2 auf die neue Signatur umgeschrieben (inkl. der beiden von Task 3 verlangten neuen Faelle: tenantId beim Anlegen, zwei Nutzer = zwei Zeilen), damit beide Tasks durchgaengig kompilieren - **Files modified:** apps/api/src/tenders/tender-email-config.service.spec.ts - **Verification:** 8/8 Tests gruen in Task 2 wie in Task 3 - **Committed in:** 05b1d29 (Task 2 commit, vorgezogen aus Task 3) --- **Total deviations:** 2 auto-fixed (beide Rule 3 — Kompilierfaehigkeit) **Impact on plan:** Beide Anpassungen waren fuer die Korrektheit der eigentlichen Plan-Aenderung zwingend notwendig (der Controller/Service-Vertrag aendert sich, angrenzende Tests muessen mitziehen). Kein Scope Creep — beide Dateien testen ausschliesslich das, was der Plan ohnehin verlangt. ## Issues Encountered - Lokaler Postgres-Container (`tessera-ctl-db-1`) lief zu Sessionbeginn nicht — gestartet und ueber die dokumentierte Container-IP (172.19.0.2, `tessera:tessera_dev`) angebunden, wie in den Projektnotizen vorgesehen - Zwei bereits vorhandene, noch nicht angewendete Migrationen (`20260811120000_doe_notice_url_backfill`, `20260811140000_encrypt_ldap_bind_password`) mussten vor der neuen Migration per `migrate deploy` nachgezogen werden — kein Blocker, nur Reihenfolge ## User Setup Required None — keine externe Dienstkonfiguration erforderlich. Die Migration ist bisher nur lokal angewendet; das Ausrollen auf dem Testserver (192.168.13.12) bleibt bewusst Nutzeraktion beim naechsten Deploy (Projektregel: kein `docker compose pull/up/rebuild` durch Claude auf dem Testserver). ## Next Phase Readiness - Die duenne Bahn (Datenbank -> Migration -> Dienst -> Endpunkt -> Seite) steht und ist bewiesen (335/335 `src/tenders`-Tests, 603/603 API gesamt, 192/192 Web gesamt, beide Typpruefungen fehlerfrei) - Plan 17-02 (RSS-Feeds, Besitzer-Feld) kann auf denselben Checkpoint-Freigaben aufbauen — die Nutzerfreigabe deckte explizit beide Datenbank-Umbauten der Phase ab - Offen: Browser-Gegenprobe fuer `/modules/tender-radar/my-sources` mit zwei Konten desselben Mandanten (WINDOWS.md #7, unrun-verify) — vor `/gsd-ship` nachzuholen - Offen: Rollout auf dem Testserver bleibt Nutzeraktion; die Migration liegt bereit, ist aber dort noch nicht angewendet ## Self-Check: PASSED Alle im SUMMARY genannten Dateien existieren auf der Festplatte; beide Task-Commit-Hashes (`05b1d29`, `55ceb24`) sind im Git-Log auffindbar. --- *Phase: 17-eigene-ausschreibungs-quellen-je-nutzer* *Completed: 2026-08-12*