docs(17): create phase plan
This commit is contained in:
@@ -0,0 +1,398 @@
|
||||
---
|
||||
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>
|
||||
Reference in New Issue
Block a user