399 lines
24 KiB
Markdown
399 lines
24 KiB
Markdown
---
|
|
phase: 17-eigene-ausschreibungs-quellen-je-nutzer
|
|
plan: 01
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- 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
|
|
autonomous: false
|
|
requirements: [SRC-01, SRC-03]
|
|
user_setup: []
|
|
|
|
estimate:
|
|
tokens: 60000
|
|
raw_tokens: 60000
|
|
tasks: 3
|
|
confidence: low
|
|
|
|
must_haves:
|
|
truths:
|
|
- "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."
|
|
artifacts:
|
|
- 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
|
|
key_links:
|
|
- "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."
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
|
@$HOME/.claude/gsd-core/templates/summary.md
|
|
</execution_context>
|
|
|
|
<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
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="checkpoint:decision" gate="blocking">
|
|
<name>Task 1: Freigabe fuer die beiden Datenbank-Umbauten dieser Phase</name>
|
|
<decision>Darf ich die Tabellenstruktur fuer Postfaecher und RSS-Feeds jetzt umbauen?</decision>
|
|
<context>
|
|
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.
|
|
</context>
|
|
<options>
|
|
<option id="weiter">
|
|
<name>Weiter — Umbau durchfuehren</name>
|
|
<pros>Die Phase kann wie besprochen gebaut werden; jeder bekommt sein eigenes Postfach.</pros>
|
|
<cons>Der Schritt ist nur mit einer weiteren Aenderung rueckdrehbar.</cons>
|
|
</option>
|
|
<option id="stopp">
|
|
<name>Stopp — erst Sicherung, dann neu ansetzen</name>
|
|
<pros>Kein Risiko, bis eine Sicherung vorliegt.</pros>
|
|
<cons>Die Phase pausiert.</cons>
|
|
</option>
|
|
</options>
|
|
<resume-signal>Antworten Sie mit "weiter" oder "stopp".</resume-signal>
|
|
</task>
|
|
|
|
<task type="tracer" tdd="true">
|
|
<name>Task 2: Ein eigenes Postfach je Nutzer — durchgehend von der Datenbank bis zur Seite</name>
|
|
<precondition>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").</precondition>
|
|
<reversibility rating="one-way">Die Migration schreibt Bestandsdaten um und entfernt eine Eindeutigkeitsregel; ein Rueckweg braucht eine zweite Migration. Freigabe erfolgt in Task 1.</reversibility>
|
|
<files>
|
|
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
|
|
</files>
|
|
<read_first>
|
|
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
|
|
</read_first>
|
|
<behavior>
|
|
- 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).
|
|
</behavior>
|
|
<action>
|
|
**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.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/vicolab/projects/tessera-ctl/apps/api && npx prisma validate && npx prisma migrate status</automated>
|
|
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/api type-check && pnpm --filter @tessera/web type-check</automated>
|
|
<automated>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</automated>
|
|
<human-check>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.</human-check>
|
|
</verify>
|
|
<done>
|
|
`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.
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 3: Der Abruf holt alle Postfaecher — und beweist es mit Tests</name>
|
|
<files>
|
|
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
|
|
</files>
|
|
<read_first>
|
|
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
|
|
</read_first>
|
|
<behavior>
|
|
- 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.
|
|
</behavior>
|
|
<action>
|
|
**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.
|
|
</action>
|
|
<verify>
|
|
<automated>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</automated>
|
|
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/api exec vitest run src/tenders</automated>
|
|
</verify>
|
|
<done>
|
|
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.
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<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>
|
|
|
|
<verification>
|
|
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).
|
|
</verification>
|
|
|
|
<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>
|
|
|
|
<output>
|
|
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.
|
|
</output>
|