--- phase: quick-261008-dts plan: 01 subsystem: domains tags: [autodns, domains, nestjs, nextjs, prisma, rls, money-safety] status: complete requires: - module-registry (ModuleGuard, UseModule, ModuleManage) - crypto (CryptoService, global) - prisma tenant extension (forTenant) provides: - Modul Domains (Slug domains) mit AutoDNS-Anbindung Demo/Live - Kunden, Kontakt-Zuordnung, Domainliste, Verfügbarkeit, Registrierung, Aufträge affects: - docs/mandantentrennung-zugriffsklassifikation.md - CHANGELOG.md, docs/anleitung-anwender.md, docs/anleitung-administration.md tech-stack: added: [] patterns: - atomarer Anspruch (updateMany where status DRAFT, count === 1) vor dem Netzaufruf - ein Client für alle AutoDNS-Aufrufe (feste Hosts, Takt 350 ms, kein Retry) - Pull-Abgleich offener Aufträge statt Cron key-files: created: - apps/api/src/domains/autodns-client.ts - apps/api/src/domains/autodns-parse.ts - apps/api/src/domains/domain-name.ts - apps/api/src/domains/domains-settings.service.ts - apps/api/src/domains/domains-directory.service.ts - apps/api/src/domains/domains-orders.service.ts - apps/api/prisma/migrations/20261008120000_domains_autodns/migration.sql - apps/web/src/app/(portal)/modules/domains/page.tsx - apps/web/src/app/(portal)/modules/domains/components/RegisterTab.tsx - apps/web/src/app/(portal)/modules/domains/components/OrdersTab.tsx modified: - apps/api/src/domains/domains.controller.ts - CHANGELOG.md decisions: - "Aufträge des nicht aktiven Systems werden beim Abgleich nicht angefasst (Zugangsdaten gehören zum aktiven System); nur der SUBMITTING-zu-UNKNOWN-Übergang gilt für alle." - "Beim Abschicken wird der Entwurf VOR dem Anspruch gelesen, damit genau der bestätigte Inhalt hinausgeht; der Zugang wird vor dem Anspruch geladen, damit ein fehlender Zugang nie einen Auftrag in SUBMITTING stehen lässt." - "HTTP-Antwort ohne Umschlag (Gateway 502/504) zählt als UNKNOWN, nicht als FAILED; FAILED nur bei ausdrücklicher AutoDNS-Ablehnung (auth, forbidden, rate-limit, business)." - "UNKNOWN wird beim Abschicken ohne lastCheckedAt gespeichert; erst ein Abgleich mit Ergebnis setzt es und schaltet das Verwerfen frei. Ein Fehler beim Nachsehen setzt es nicht." - "Auftragssuche beim Abgleich nimmt nur Aufträge mit lesbarer Anlagezeit ab 5 Minuten vor der Bestätigung; ohne Zeit zählt ein Auftrag nicht (lieber UNKNOWN als ein falscher Treffer)." metrics: tasks: 3 completed: 2026-10-08 actuals: tokens: 109600 tasks: 3 commits: 3 plan_head_before: 6b0f840c1cf1ad09d593e9f607b5230541fdfb2b plan_head_after: d332246bdead2b5ec87156dd610945687c672016 --- # Phase quick-261008-dts Plan 01: Modul Domains (AutoDNS) Summary Neues Modul „Domains“ mit verschlüsselter Demo-/Live-Anbindung an AutoDNS, Kunden- und Kontaktzuordnung, Domainliste und einer Registrierung, die pro Auftrag höchstens einmal an AutoDNS geht und bei unklarem Ausgang auf „Ergebnis ungeklärt“ stehen bleibt, statt zu raten. ## Commits | Aufgabe | Commit | Inhalt | |---|---|---| | 1 (Tracer) | 1f1c984 | Migration (4 Tabellen mit Zeilenschutz), AutoDNS-Client, Einstellungen, Verbindungstest, Modulseite mit Reiter Einstellungen, Registrierungen (Loader, Icon, Navigationstitel) | | 2 | 450218f | Kunden, Kontakte live aus AutoDNS (Seiten, 60-s-Zwischenspeicher), Zuordnung, Kontakt anlegen, Domainliste, Reiter Domains/Kontakte/Kunden | | 3 | d332246 | Verfügbarkeit, Entwurf, verbindliches Abschicken mit atomarem Anspruch, Auftragsverfolgung und Abgleich, Reiter Registrieren/Aufträge, Changelog, Anwender- und Administrationshandbuch, Mandantenschutz-Inventar | Nichts gepusht. Docs-Artefakte (PLAN, RESEARCH, SUMMARY, STATE) sind nicht committet. ## Aufgabe 3 im Detail (Geldsicherheit) - **Anspruch vor dem Netz:** `submitOrder` führt `updateMany` mit `id, tenantId, status: 'DRAFT', environment: ` aus. Nur bei `count === 1` geht genau ein `POST /domain` (ohne Query, ohne Wiederholung) hinaus. Wer `count !== 1` bekommt, lädt die Zeile neu und erhält 404, 409 `environmentChanged` oder 409 `alreadySubmitted`. - **Ausgang:** Auftragsnummer vorhanden gleich SUBMITTED. Ausdrückliche AutoDNS-Ablehnung (auth, forbidden, rate-limit, business, auch HTTP 200 mit `status.type` ERROR) gleich FAILED mit `openKey` null. Zeitablauf, Netz, TLS, unlesbare oder umschlaglose Antwort gleich UNKNOWN, nie zurück auf DRAFT. - **Eindeutigkeit:** `openKey` (Domain) mit `@@unique([tenantId, environment, openKey])`; FAILED und CANCELED geben ihn frei, SUCCESS behält ihn bewusst. - **Abgleich:** Pull beim Öffnen des Reiters „Aufträge“, bei „Aktualisieren“ und alle 30 s bei offenen Aufträgen und sichtbarer Seite; höchstens 20 Aufträge je Lauf, am längsten ungeprüfte zuerst; ein Fehler bei einem Auftrag hält die anderen nicht auf. SUBMITTING älter als 120 s wird UNKNOWN. UNKNOWN: erst `GET /domain/{name}`, dann `POST /job/_search`. - **Verwerfen:** Entwurf jederzeit; UNKNOWN erst nach einem Abgleich mit Ergebnis (`lastCheckedAt` gesetzt), im Browser mit zusätzlicher Warnung. - **Browser:** `useRef`-Sperre gegen Doppelklick; nach einem Fehler beim Abschicken bleibt der Knopf gesperrt, bis „Zurück“ gewählt wird (der Auftrag könnte angekommen sein). ## Messwerte der Prüfkette (Aufgabe 3) | Segment | Ergebnis | |---|---| | `pnpm --filter @tessera/api test` | 133 Dateien, 2423 Tests grün | | `pnpm --filter @tessera/web test` | 132 Dateien, 1455 Tests grün | | `tsc --noEmit` api und web | grün | | `biome check` (alle angefassten Dateien) | grün | | rls-coverage, rls-access-inventory | 35 Tests grün (domains: 0/30/0, Paare 104) | | Rollen-Decorator im Controller | keiner | | de/en-Schlüsselgleichheit `domains.*`, Wortprüfung (Mandant, Lizenz) | 207 Schlüssel, gleich, ohne Treffer | | CHANGELOG-/Handbuch-Greps | ok | | Neuaufbau `docker compose up -d --build api web` | api, web, db laufen; „Domains module seeded in registry“; Routen `orders/refresh` vor `orders/:id/submit`; keine ausstehenden Migrationen; `GET customers` und `GET orders` geben `[]`; `POST availability` ohne Zugang gibt 409 `notConfigured` | | Gegenprobe Doppelbestellung | Anspruchsprüfung testweise ausgebaut: Test „zwei gleichzeitige Bestätigungen“ und Test „Systemwechsel zwischen Lesen und Anspruch“ schlugen fehl; danach zurückgebaut | ## Abweichungen vom Plan ### Automatisch behoben und ergänzt **1. [Regel 2 - Fehlende Absicherung] Kontakt-Prüfung beim Entwurf** - **Ort:** `createOrder` - **Problem:** Ohne Prüfung könnte ein Entwurf Kontaktnummern enthalten, die bei AutoDNS nicht (mehr) existieren. - **Lösung:** Alle vier Kontaktnummern werden gegen die Kontaktliste geprüft (`findContactsByIds`), sonst 400 `contactNotFound`. Grenze: bei mehr als 2000 Kontakten kann ein vorhandener Kontakt außerhalb der gelesenen Liste fälschlich abgelehnt werden; die Meldung rät zum Neu-Einlesen. **2. [Regel 3 - Blockierend] Zeitlimit im Test einstellbar** - `DomainsSettingsService.transport` bekam das Feld `timeoutMs` (nur für Tests), damit der Zeitablauf-Fall mit 20 ms getestet werden kann; im Betrieb bleibt es leer (20 s). `HOSTNAME_PATTERN` wird exportiert und im Auftragsdienst wiederverwendet. **3. [Regel 1 - Fehler im eigenen Entwurf] Zusätzliche Absicherungen im Abgleich** - Ein Fehler beim Nachsehen (Anmeldung, Zeitablauf, Gateway) setzt `lastCheckedAt` nicht, damit „Verwerfen“ nie auf einer Prüfung beruht, die nichts geprüft hat. Nur eine ausdrückliche AutoDNS-Antwort „gibt es nicht“ (Fehlerart business) zählt als Ergebnis. - Alle Schreibzugriffe des Abgleichs sind an den erwarteten Zustand gebunden (`status` in der `where`-Klausel), damit zwei gleichzeitige Abgleiche sich nicht überschreiben. **4. [Hinweis] Reiter Aufträge für alle, auch ohne Einrichtung** - Der Reiter ist immer sichtbar (Liste kommt aus der Datenbank); der Abgleich überspringt ihn still, wenn das aktive System nicht eingerichtet ist. **5. [Aufräumen] Versehentliche Fremdformatierung rückgängig gemacht** - Ein Formatierungslauf hatte `module-manage-handlers.spec.ts` in unbeteiligten Zeilen umgebrochen; die Datei wurde zurückgesetzt und nur um die sechs Handlernamen ergänzt. ### Nicht behoben (vorbestehend) Keine fehlschlagenden Tests. `biome organizeImports` meldet in `module-manage-handlers.spec.ts` eine bereits vor dieser Arbeit vorhandene Importreihenfolge; bewusst nicht angefasst. ## Threat-Status | ID | Stand | |---|---| | T-dts-01 Passwort | umgesetzt (Aufgabe 1); Antworten tragen nur `hasPassword` | | T-dts-02 Rechte | alle Schreib- und Verwalten-Wege tragen `@ModuleManage('domains')`, kein Rollen-Decorator; Controller- und Handler-Spec grün | | T-dts-03 Doppelbestellung | umgesetzt und mit Gegenprobe belegt (siehe oben) | | T-dts-04 SSRF | feste Hosts, `redirect: 'error'`, Pfadprüfung | | T-dts-05 Organisationstrennung | `forTenant` überall, RLS-Gates grün | | T-dts-06 Abfragegrenze | Takt 350 ms, höchstens 20 Aufträge je Abgleich, Verbindungstest ein Aufruf | | T-dts-07 Demo/Live | System in Anspruch und Zuordnung, Wechsel nur mit Bestätigung | | T-dts-08 Routen-Reihenfolge | `orders/refresh` vor `orders/:id/...`, Spec prüft | | T-dts-09 Eingaben | `normalizeDomainName`, Validatoren, `ParseUUIDPipe` | | T-dts-10 Fehlertexte | nur `messages[].text`, gekürzt; AutoDNS 401/403 als 502 | ## Bekannte Stubs Keine. Die Anzeige „Preis nicht ermittelbar“ ist gewollt (AutoDNS nennt keinen Preis). ## Prüfung gegen AutoDNS-Demo Voraussetzung: Demo-Zugangsdaten eintragen (Domains, Einstellungen, Zugang Demo-System, Benutzername, Passwort, Kontext). System bleibt auf „Demo-System“. Jeder Punkt schließt eine offene Annahme aus der Recherche. | Nr. | Annahme | Prüfung in der Oberfläche | Wenn es abweicht | |---|---|---|---| | 1 | A1: Welcher Demo-Kontext funktioniert (1 oder 4) | Einstellungen, Zugang Demo-System: Kontext 4 eintragen, speichern, „Verbindung testen“; bei Fehler 1 versuchen. Je Versuch genau einmal klicken (Sperrgefahr) | Wert merken und in die Handbücher übernehmen | | 2 | A6/A9: Anmeldung ohne Zwei-Faktor, Test gelingt | „Verbindung testen“ zeigt „Verbindung erfolgreich“ | bei Anmeldefehler Benutzer ohne 2FA verwenden | | 3 | A2: Kontaktfelder und Telefonformat | Reiter Kontakte, „Neuer Kontakt“: erst Person, dann Organisation anlegen (Telefon +49 30 123456). Meldung „Der Kontakt wurde bei AutoDNS angelegt“, Kontakt erscheint nach „Aus AutoDNS neu einlesen“ | Fehlermeldung von AutoDNS lesen (steht im Formular); Pflichtfelder oder Telefonformat anpassen | | 4 | A3: Verfügbarkeitsprüfung liefert genau die gefragte Domain mit WHOIS-Status und Preis | Reiter Registrieren: eine sicher freie Domain (zufälliger Name) und eine belegte (zum Beispiel denic.de) prüfen. Frei zeigt „… ist frei“ mit Preis; belegt zeigt „nicht frei“ mit Status. Außerdem eine Umlautdomain prüfen | Steht immer „nicht frei“ mit Status „NO_RESULT“, liefert DomainStudio den Namen anders (Suchwort, Endung) und `parseDomainStudio` oder die Anfrage ist anzupassen. Fehlt nur der Preis, erscheint „Preis nicht ermittelbar“: Preisform prüfen | | 5 | A4/A5: Registrierung liefert Auftragsnummer, `{ id }` genügt als Kontaktverweis | Zusammenfassung erstellen, Häkchen setzen, „Jetzt verbindlich registrieren“. Erwartet: „wird bearbeitet“, Reiter Aufträge zeigt „In Bearbeitung“ | „Fehlgeschlagen“ mit Text von AutoDNS: Text lesen (Kontaktverweis, Nameserver, Pflichtfelder). Steht ein Auftrag ohne Nummer auf „In Bearbeitung“ und ändert sich nie, ist die Fundstelle der Nummer anders (`extractJobFromSubmit`) | | 6 | Auftrag erreicht SUCCESS, Tessera folgt | Reiter Aufträge, „Aktualisieren“ (oder 30 s warten): Stand wechselt auf „Registriert“ | bleibt er „In Bearbeitung“, in AutoDNS den Auftrag ansehen; „Rückfrage nötig“ heißt Status SUPPORT | | 7 | D-J: Domainliste mit Inhaber, Ablaufdatum, `registryStatus` | Reiter Domains, „Aus AutoDNS neu laden“: die neue Domain erscheint mit Inhaber, Ablaufdatum und Status (Aktiv, In Bearbeitung …). Kunde nach Zuordnung des Inhaber-Kontakts | Status als Rohwert angezeigt: `registryStatus` hat andere Werte, Beschriftungen nachziehen. Ablaufdatum „–“: Feld `expire` fehlt in der Antwort | | 8 | A8: `.de` mit Standard-Nameservern besteht die Nameserver-Prüfung | Standard-Nameserver in den Einstellungen eintragen (bei AutoDNS eingerichtete), eine freie `.de`-Domain registrieren | „Fehlgeschlagen“ mit Nameserver-Meldung: Nameserver einrichten oder korrigieren | | 9 | Neu in Aufgabe 3: Auftragssuche (`POST /job/_search`, Filter `object`, Anlagezeit) und `GET /domain/{name}` für nicht Gefundenes | Schwer künstlich auszulösen. Wenn ein Auftrag je auf „Ergebnis ungeklärt“ steht: „Aktualisieren“ klicken. Erwartet: Tessera findet die Domain (Registriert), einen Auftrag (In Bearbeitung) oder setzt die Prüfung (dann ist „Verwerfen“ möglich) | Bleibt „Verwerfen“ trotz Abgleich gesperrt, liefert `GET /domain/{name}` für Unbekanntes keinen business-Fehler; Antwortform prüfen | | 10 | Neu in Aufgabe 3: Umlautdomain wird als Punycode gesucht und bestellt | Umlautdomain prüfen; Anzeige zeigt die Umlautform | DomainStudio erwartet ggf. die Unicode-Form im Suchwort | | 11 | D-J: Wert für „Kündigung vorgemerkt“ | Eine Domain mit vorgemerkter Kündigung in der Liste ansehen (falls vorhanden) | Werte von `cancelationStatus` prüfen (NONE und NOT_SET gelten als „keine“) | ## Browser-Prüfschritte für den Orchestrator (dunkler Modus bevorzugt) Lokal: `http://localhost:3000`, Anmeldung `admin` / `admin123`. Playwright-Prüfung mit Theme-Knopf auf dunkel. 1. **Aktivieren:** Marktplatz, Modul „Domains“ aktivieren. Erwartet: Modul in der Seitenleiste (Gruppe Domains), Erde-Symbol. 2. **Freigabe Benutzen gegen Verwalten:** Mit einem normalen Benutzer mit „Benutzen“ anmelden: Reiter Domains, Kontakte, Kunden, Aufträge sichtbar; kein Registrieren, kein Einstellungen, kein „Neuer Kontakt“, keine Zuordnung, keine Kunden-Knöpfe. Mit Admin oder „Verwalten“: zusätzlich Registrieren und Einstellungen. 3. **Einstellungen:** Ohne Zugang zeigt die Kopfzeile „AutoDNS nicht eingerichtet“ mit Hinweis und Knopf „Zu den Einstellungen“. Passwort eintragen, speichern, Seite neu laden: Passwortfeld leer mit Platzhalter „Gespeichert – leer lassen …“. Live-Kontext ist mit 4 vorbelegt. Auswahl „Live-System“: Bestätigungsfeld erscheint; „Abbrechen“ stellt Demo zurück; erst „Live-System verwenden“ speichert, Kopfzeile zeigt dann „Live-System – Registrierungen kosten Geld“. Wieder auf Demo zurückstellen. Nameserver (zwei) eintragen und speichern. 4. **Kontakte:** Liste, Suche, Filter „Nicht zugeordnet“, „Nach Kunde gruppieren“; Kontakte ankreuzen, Kunden wählen, „Zuordnen“; „Zuordnung entfernen“; „Neuer Kontakt“ mit ungültiger E-Mail und mit Telefon ohne „+“ (Fehler sichtbar), dann gültig speichern. 5. **Kunden:** Kunden anlegen, einen als „Eigene Firma“ markieren (Kennzeichnung erscheint, bei einem zweiten wandert sie); Kunden mit Kontakten löschen: Meldung „Diesem Kunden sind noch Kontakte zugeordnet …“. 6. **Domains:** Liste, Gruppen pro Kunde mit „Nicht zugeordnet“ zuletzt, Kundenfilter blendet andere Gruppen aus, Datum als TT.MM.JJJJ. 7. **Registrieren:** Ungültiger Name zeigt Meldung; freie und belegte Domain; Kontaktauswahl gruppiert nach Kunde, Voreinstellung Admin gleich Inhaber, Technik/Zone gleich eigene Firma; Nameserver hinzufügen/entfernen (2 bis 6); „Zusammenfassung anzeigen“ zeigt System-Kennzeichnung, Preis oder „Preis nicht ermittelbar“; „Jetzt verbindlich registrieren“ bleibt gesperrt bis zum Häkchen; **Doppelklick auf den Knopf** darf nur einen Auftrag erzeugen (Reiter Aufträge zeigt eine Zeile, Server-Log zeigt ein einziges „Registrierung abgeschickt“). Vorsicht: Mit Demo-Zugang ohne echte Daten antwortet AutoDNS mit einem Fehler, die Zeile steht dann auf „Fehlgeschlagen“ oder „Ergebnis ungeklärt“; beides ist ein gültiger Ausgang. 8. **Aufträge:** Reiter zeigt Zeilen mit System (Demo/Live), Stand, Bestätigt von/am, Fehlertext; „Aktualisieren“; „Verwerfen“ nur für Verwalter bei Entwurf und geprüftem unklarem Auftrag, mit Rückfrage. ## Self-Check: PASSED - Dateien vorhanden: domain-name.ts, domains-orders.service.ts, dto/domains-order.dto.ts, RegisterTab.tsx, OrdersTab.tsx, order-status.ts (alle von `git status` als hinzugefügt bestätigt). - Commits vorhanden: 1f1c984, 450218f, d332246 liegen in der Historie von HEAD (`git rev-list --count 6b0f840..HEAD` gleich 3).