docs(quick-261008-dts): Modul Domains mit AutoDNS-Anbindung
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+217
@@ -0,0 +1,217 @@
|
||||
# Quick 261008-dts: Modul "Domains" (AutoDNS) - Research
|
||||
|
||||
**Researched:** 2026-10-08
|
||||
**Domain:** AutoDNS/InterNetX Domainrobot JSON API + Tessera-Modulmuster
|
||||
**Confidence:** MEDIUM (Endpunkte und Schemas HIGH aus OpenAPI; reale Antworten der Auftrags-Endpunkte und Demo-Kontext nicht ohne Zugangsdaten pruefbar)
|
||||
|
||||
Es gibt keine CONTEXT.md fuer diese Aufgabe. Es wird kein neues npm-Paket benoetigt (HTTP ueber `undici` 7.28.0, `class-validator`, `@nestjs/schedule` sind bereits in `apps/api/package.json`), daher entfaellt das Package Legitimacy Audit. [VERIFIED: apps/api/package.json Zeilen 25, 32, 52]
|
||||
|
||||
## Project Constraints (aus CLAUDE.md / Memory)
|
||||
- UI-Texte Deutsch, siezen; keine "Mandant"-Begriffe in neuen UI-Texten; keine Lizenzierungs-/Mandantenfaehigkeits-Themen anschneiden.
|
||||
- Keine firmenspezifischen Werte hart in generische Admin-UI (Standard-Nameserver also nur als Einstellung, nicht als Konstante).
|
||||
- Neue Module: `@UseModule(slug)` auf Klassenebene ist Pflicht, Schreib-/Pruefrouten zusaetzlich `@ModuleManage(slug)`; NIE ein Rollen-Decorator auf Verwalten-Handlern.
|
||||
- Statische Routen VOR `:id`-Routen (Unit-Tests fangen das nicht, Reihenfolge im Controller-Spec festschreiben).
|
||||
- Zugangsdaten nie an den Client zurueckgeben (LDAP-Muster: Maske `'********'`).
|
||||
- Tests mit gemockter API; kein Docker-Deploy auf Testserver durch Claude.
|
||||
- Alle Aenderungen laufen ueber einen GSD-Workflow.
|
||||
|
||||
## Summary
|
||||
|
||||
Die AutoDNS-JSON-API deckt alle vier Etappe-1-Funktionen ab. Es gibt KEINEN `POST /domain/_check`; die Verfuegbarkeitspruefung laeuft ueber `POST /domainstudio` (WHOIS-Dienst je Domain, optional PRICE). `POST /domain` ist asynchron und liefert einen Job; der Verlauf wird ueber `GET /job/{id}` verfolgt. Die Antwort-Huelle ist fuer alle Routen gleich (status/stid/object/data/messages).
|
||||
|
||||
Im Code ist das Nextcloud-Status-Modul (Controller/Seed/Modul) plus Handelsware-Datev (Einstellungs-Tab, Singleton-Konfiguration) das beste Vorbild; die AES-Verschluesselung kommt aus `CryptoService`. Der groesste fachliche Risikopunkt ist Geldsicherheit: `POST /domain` darf pro Bestellung hoechstens einmal abgeschickt werden, und ein Timeout heisst NICHT "nicht bestellt".
|
||||
|
||||
**Primary recommendation:** Zwei Umgebungen (DEMO/LIVE) mit je eigenem Zugang, feste Basis-URLs (keine freie URL-Eingabe), lokale Bestelltabelle mit atomarem Statuswechsel DRAFT -> SUBMITTING vor dem API-Aufruf, Kontakte und Domains live aus AutoDNS lesen und nur die Kundenzuordnung lokal halten.
|
||||
|
||||
## Architectural Responsibility Map
|
||||
|
||||
| Capability | Primary Tier | Secondary | Rationale |
|
||||
|---|---|---|---|
|
||||
| AutoDNS-Aufrufe, Zugangsdaten | API/Backend | - | Passwort darf nie in den Browser; Basic Auth serverseitig |
|
||||
| Kundenzuordnung Kontakte | Database | API | Nur lokal vorhanden, AutoDNS kennt "Kunde" nicht |
|
||||
| Bestell-Zustand (Idempotenz) | Database + API | - | Atomarer Statuswechsel in DB schuetzt vor Doppelbestellung |
|
||||
| Job-Status-Nachverfolgung | API (Abruf beim Oeffnen + Cron) | Browser (Polling der eigenen API) | Browser spricht nie mit AutoDNS |
|
||||
| Filter/Gruppierung nach Kunde | Browser | API | Kleine Datenmenge, Zusammenfuehren im API-Dienst |
|
||||
| Berechtigung | API (ModuleGuard) | Browser (`useCanManageModule`, nur Anzeige) | Bindend ist nur die API |
|
||||
|
||||
## AutoDNS JSON API (verifiziert)
|
||||
|
||||
Quelle der Schemas: OpenAPI 2.0 `https://raw.githubusercontent.com/InterNetX/domainrobot-api/master/src/domainrobot.json` (Info version `v1`, host `api.autodns.com`, basePath `/v1`), gelesen und ausgewertet am 2026-10-08. [VERIFIED: openapi domainrobot.json]
|
||||
|
||||
### Basis, Authentisierung, Limits
|
||||
| Punkt | Wert | Quelle |
|
||||
|---|---|---|
|
||||
| Live-URL | `https://api.autodns.com/v1` | [CITED: help.internetx.com/x/cQbj "Interface addresses"] |
|
||||
| Demo-URL | `https://api.demo.autodns.com/v1` | [CITED: help.internetx.com/x/cQbj; java-domainrobot-sdk README] |
|
||||
| Erreichbarkeit | Beide Hosts antworten von hier mit HTTP 401 ohne Zugangsdaten | [VERIFIED: curl-Probe 2026-10-08, ohne Zugangsdaten] |
|
||||
| Auth | HTTP Basic (Benutzer/Passwort) + Header `X-Domainrobot-Context: <int>`; Alternative Session (`X-Domainrobot-SessionId`), von InterNetX nicht empfohlen ausser bei Zwangs-Timeout | [CITED: help.internetx.com Suchergebnis "Login and Authentication"; js-sdk `DomainRobotService.js` setzt Basic + Context-Header] |
|
||||
| User-Agent | Pflicht ("mandatory") - eigenen setzen, z. B. `Tessera/<version>` | [CITED: help.internetx.com/x/cQbj] |
|
||||
| Rate-Limit | "Only 3 requests per second and IP" - Aufrufe serialisieren, Listen seitenweise, kein paralleles Fan-out | [CITED: help.internetx.com/x/cQbj] |
|
||||
| Verbindungstest | `GET /hello` ("performs no operation, used only for testing the connection and authentication credentials") | [CITED: help.internetx.com/x/cQbj; OpenAPI `/hello`] |
|
||||
| Live-Context | Standard `4` | [CITED: wisecp-Doku und help.internetx.com (Plugin-Seite): "default context for AutoDNS is 4"] |
|
||||
| Demo-Context | UNKLAR: Drittanbieter nennt `1` fuer Demo, die offiziellen SDK-Beispiele nutzen die Demo-URL mit `"4"` | [ASSUMED] - Kontext deshalb je Umgebung als freie Ganzzahl speichern, nicht hartkodieren |
|
||||
| 2FA | OpenAPI kennt Header `X-Domainrobot-2FA-Token`; Hilfeseite sagt 2FA nur fuer XML | [ASSUMED] Widerspruch - dedizierten API-Benutzer OHNE 2FA empfehlen |
|
||||
| `X-Domainrobot-Demo` (boolean Header) | in OpenAPI vorhanden, Wirkung undokumentiert | NICHT verwenden; Umgebung nur ueber die Basis-URL waehlen |
|
||||
|
||||
Fehlerhafte Anmeldung: Live liefert 401 mit `{"messages":[{"code":"EF00202","text":"User does not exist or password incorrect.","status":"ERROR",...}],"status":{"code":null,"text":null,"type":"ERROR"},"stid":"..."}`; Demo ohne Basic-Header liefert 401 `EF1321001 "The authsession could not be found."`. [VERIFIED: curl-Probe 2026-10-08] Wichtig: wiederholte falsche Anmeldungen koennen den Benutzer sperren [ASSUMED] - Verbindungstest nie in Schleife/Retry.
|
||||
|
||||
### Antwort-Huelle
|
||||
```json
|
||||
{ "status": {"code":"S0301","text":"...","type":"SUCCESS"},
|
||||
"stid": "20180915-app1",
|
||||
"object": {"type":"contact","value":"100101","summary":1},
|
||||
"messages": [ {"code":"...","text":"...","status":"ERROR","objects":[...]} ],
|
||||
"data": [ { } ] }
|
||||
```
|
||||
- `status.type` Enum: `SUCCESS`, `ERROR`, `NOTIFY`, `NOTICE`, `NICCOM_NOTIFY`. [VERIFIED: OpenAPI StatusType]
|
||||
- Code-Praefixe: `S` Erfolg, `E` Fehler, `N` Benachrichtigung ("accepted, further processing required"). [CITED: help.internetx.com/x/cQbj]
|
||||
- Die Hilfeseite zeigt das Feld als `resultCode`, OpenAPI und die reale 401-Antwort nutzen `code`. Parser muss `status.code ?? status.resultCode` lesen. [VERIFIED: Probe + OpenAPI; Abweichung CITED]
|
||||
- `object.summary` = Gesamtzahl bei Listen (Paging). [VERIFIED: OpenAPI ResponseObject "amount of objects found in list tasks"]
|
||||
- HTTP-Status ist nicht verlaesslich fuer Fachfehler: IMMER `status.type === 'ERROR'` bzw. `messages[].status` pruefen, auch bei HTTP 200. [ASSUMED]
|
||||
- Fehlertexte an den Client nur gekuerzt und ohne Header/Zugangsdaten durchreichen (`messages[].text`).
|
||||
|
||||
### Routen und minimale Nutzlasten
|
||||
| Zweck | Route | Nutzlast / Hinweis |
|
||||
|---|---|---|
|
||||
| Kontakt anlegen | `POST /contact` | `{"type":"PERSON","fname":"Max","lname":"Muster","address":["Musterstr. 1"],"pcode":"12345","city":"Berlin","country":"DE","email":"x@example.com","phone":"+49 30 123456"}`; `type` Enum `PERSON`, `ORG`, `ROLE`; `ORG` braucht `organization`; `address` ist ein ARRAY; `alias` optional (wird generiert); Antwort `data[0].id` (Ganzzahl). Pflichtfelder sind in der OpenAPI nicht als `required` markiert -> obige Menge ist [ASSUMED], gegen Demo pruefen. `extensions` (z. B. `general.gender`, `it.entityType`) nur TLD-abhaengig, fuer `.de` nicht noetig [ASSUMED]. |
|
||||
| Kontakte listen | `POST /contact/_search` | Body `Query`: `{"filters":[{"key":"lname","value":"%test%","operator":"LIKE"}],"view":{"limit":100,"offset":0},"orders":[{"key":"lname","type":"ASC"}]}`; leere `filters` = alle. Filterbare Schluessel: country, pcode, city, type, title, lname, alias, state, id, email, fname, address, created, phone, organization, comment, updated ... Operatoren: EQUAL, NOT_EQUAL, LIKE, ILIKE, GREATER, LESS, IN, IS_NULL ... [VERIFIED: OpenAPI + js-sdk ContactList.js] |
|
||||
| Kontakt lesen/aendern | `GET/PUT /contact/{id}` | Spaeter; `DELETE /contact/{id}` NICHT in Etappe 1 anbieten |
|
||||
| Verfuegbarkeit | `POST /domainstudio` | Es gibt keine Route `/domain/_check` (alle `/domain*`-Pfade der OpenAPI durchgesehen). Body `DomainEnvelopeSearchRequest`: `{"searchToken":"example","currency":"EUR","checkPortfolio":true,"sources":{"initial":{"tlds":["de"],"services":["WHOIS","PRICE"]}}}`; alternativ `sources.custom.domains:["example.de"]`. Antwort `data[]` = `DomainEnvelope` mit `domain`, `services.whois.data.status` (Enum `FREE`, `ASSIGNED`, `MARKET`, `PREMIUM`, `INVALID`, `ERROR`, `TIMEOUT`, `RESERVED`, `PREMIUM_CLAIM`, `CLAIM`, `OFFER`) und `services.price.data.prices[]` (`amount`, `currency`, `period`). [VERIFIED: OpenAPI Schemas]. Die genaue Kombination `initial.tlds` + `searchToken` bzw. `custom.domains` liefert ein Ergebnis fuer genau die gewuenschte Domain: [ASSUMED], gegen Demo pruefen. Nur `status === 'FREE'` zaehlt als bestellbar; alles andere (auch ERROR/TIMEOUT) = nicht bestellbar. |
|
||||
| Domain bestellen | `POST /domain` | "The operation is asynchronous and creates a job." Body (Domain): `{"name":"beispiel.de","period":{"unit":"YEAR","period":1},"ownerc":{"id":123},"adminc":{"id":123},"techc":{"id":123},"zonec":{"id":123},"nameServers":[{"name":"ns1.example.com"},{"name":"ns2.example.com"}]}`. Contacts als Objekte mit `id` (SDK-Beispiel reicht ganze Kontakt-Objekte; nur `{id}` genuegt vermutlich: [ASSUMED]). Query-Parameter `ignoreWhois`, `nsCheck`, `replyTo` - `ignoreWhois` NIE setzen. Antwort `JsonResponseDataJob`: `data[0]` = Job. Wo genau die Job-Id steht (`data[0].id`, ausserdem vermutlich `object.type="job"`/`object.value`): [ASSUMED], gegen Demo pruefen. `confirmOrder` (Nutzungsbedingungen mancher TLDs) und `period` je nach TLD: [VERIFIED: Feldbeschreibung OpenAPI]. Zusatz `nameServerEntries` nur fuer `.de` und schliesst `nameServers` aus. |
|
||||
| Job lesen | `GET /job/{id}` | Antwort `ObjectJob`: `data[0].job.status` (Enum `RUNNING`, `SUCCESS`, `FAILED`, `CANCELED`, `SUPPORT`, `DEFERRED`, `NOT_SET`, `WAIT`), `.job.subStatus`, `.job.action`, `.object` (Domain-Name), `.job.events[]`. Terminal: `SUCCESS`, `FAILED`, `CANCELED`; `SUPPORT` = Eingriff durch InterNetX noetig (als "Rueckfrage noetig" anzeigen); `WAIT`/`DEFERRED`/`RUNNING` = offen. [VERIFIED: OpenAPI Enum; Terminal-Deutung ASSUMED] |
|
||||
| Jobs suchen | `POST /job/_search` | Query mit Schluesseln `id`, `status`, `object`, `type`, `action`, `created` -> Abgleich nach Timeout (siehe Pitfall 1). |
|
||||
| Domains listen | `POST /domain/_search?keys[]=expire&keys[]=ownerc` | Body `Query` wie bei Kontakten (`view.limit/offset`). Zusatzfelder per wiederholtem Query-Parameter `keys[]` (Format `?keys[]=a&keys[]=b`): zulaessig u. a. `expire`, `ownerc`, `adminc`, `techc`, `zonec`, `nserver`, `autorenew`, `cancelationStatus`, `authinfo`, `certificate`. Statusfelder: `registryStatus` (`ACTIVE`, `HOLD`, `LOCK`, `PENDING` ...), `cancelationStatus`, `action`. [VERIFIED: OpenAPI + js-sdk DomainService.list + php-sdk DomainList.php] |
|
||||
| Domain lesen | `GET /domain/{name}` | Detail/Abgleich nach Timeout. |
|
||||
|
||||
Spaeter (Architektur offenhalten, nicht bauen): Transfer `POST /domain/_transfer`, Kuendigung `POST /domain/{name}/cancelation`, Zonen `GET/PUT /zone/{name}/{virtualNameServer}`, Auftrag bestaetigen/abbrechen `PUT /job/{id}/_confirm` / `_cancel`, Poll-Nachrichten `GET /poll`. [VERIFIED: OpenAPI paths] Deshalb den AutoDNS-Client als eigene Klasse mit allgemeiner `request(method, path, body, keys)`-Methode plus duenner Fachschicht bauen.
|
||||
|
||||
### .de-Besonderheiten (kurz)
|
||||
- Seit 25.05.2018 erhebt DENIC nur noch den Domaininhaber als personenbezogene Kontaktdaten; Admin-C/Tech-C/Zone-C entfallen bei DENIC, zusaetzlich zwei neutrale Adressen (General Request/Abuse). AutoDNS-Objekte kennen weiter `ownerc/adminc/techc/zonec`; sicher ist, den Inhaber vollstaendig mit Anschrift zu fuehren und fuer die uebrigen drei Rollen einen Kontakt zu referenzieren. [CITED: denic.de Pressemitteilung 25.05.2018 laut Suchergebnis; Abbildung auf AutoDNS-Pflichtfelder ASSUMED] - Empfehlung: alle vier IDs im Formular verlangen, Voreinstellung = Inhaber bzw. eigener Firmenkontakt als Technik-/Zonen-Kontakt.
|
||||
- `.de` verlangt 2 bis 6 funktionierende Nameserver; DENIC prueft sie bei der Registrierung (Nast-Check), unerreichbare/nicht eingerichtete Nameserver lassen den Auftrag scheitern. Parameter `nsCheck` existiert. [CITED: namecheap/ans.co.uk Suchergebnis; AutoDNS-Verhalten ASSUMED] - Auftrag `FAILED` mit `messages` verstaendlich anzeigen.
|
||||
- Regeln pro TLD unterscheiden sich stark (`extensions`); Etappe 1 auf Kern-TLDs (.de/.com/.net/.org) beschraenken oder bei fehlenden Pflichtangaben die API-Fehlermeldung durchreichen.
|
||||
|
||||
## Codebase-Integration (verifiziert durch Lesen der Dateien)
|
||||
|
||||
### Backend: Dateien als Vorlage kopieren
|
||||
| Zweck | Vorlage | Hinweis |
|
||||
|---|---|---|
|
||||
| Modul + Seed beim Start | `apps/api/src/nextcloud-status/nextcloud-status.module.ts`, `nextcloud-status.seed.ts` | `seedModule({slug, name, version, category, description:{de,en}, isSystem:true})`; `onModuleInit` mit try/catch. Kategorie: `'infrastructure'` (existiert, `proxmox`/`nextcloud-status` nutzen sie) oder `'domain-tools'` (existiert, `domaincheck` nutzt sie). [VERIFIED: nextcloud-status.seed.ts; module-categories.service.spec.ts Zeilen 17-20] Empfehlung: `'domain-tools'` zu Domaincheck, Kategorien sind laut 261003-387 durch Admins umbenennbar. |
|
||||
| Controller | `nextcloud-status.controller.ts` | `@Controller('modules/<slug>')` + `@UseModule('<slug>')` Klasse; `requireTenantId(req)` aus `req.tenantId`; Schreib-/Pruef-/Einstellungsrouten `@ModuleManage('<slug>')`; Lese-Routen ohne. |
|
||||
| Singleton-Einstellungen + Tab | `handelsware-datev.controller.ts`, `handelsware-datev.service.ts`, Web `modules/handelsware-datev/page.tsx` + `components/SettingsTab.tsx` | Tab "Einstellungen" nur bei `useCanManageModule`. |
|
||||
| Verschluesselung | `apps/api/src/crypto/crypto.service.ts` (`CryptoModule`) | `encrypt(plain)` -> `iv:authTag:ciphertext` (AES-256-GCM, Schluessel `TESSERA_ENCRYPTION_KEY`, 64 Hex). Entschluesseln-Fehler NICHT schlucken (kein stiller Wechsel auf "ohne Passwort"), wie `ldap-config.service.ts` `decryptBindPassword`. Passwort-Feld im API-Response als `'********'` bzw. `hasPassword: boolean`; leeres Feld beim Speichern = unveraendert (LDAP-Muster `dto.bindPassword \|\| config.bindPassword`). |
|
||||
| HTTP-Client | `apps/api/src/proxmox/proxmox-client.service.ts` | `undiciFetch` (NICHT globales `fetch`), `AbortController`-Timeout, Fehlerdetail gekuerzt (500 Zeichen), Fehlerklassifikation. Fuer AutoDNS: festes Basis-URL-Mapping DEMO/LIVE, kein Eintrag frei waehlbar (kein SSRF), 15-20 s Timeout, Basic-Header aus entschluesselten Daten. |
|
||||
| Hintergrunddienst (Auftragsstatus) | `nextcloud-status-scheduler.service.ts` | `OnApplicationBootstrap`, `SchedulerRegistry` + `require('cron').CronJob`-Umgehung, Ueberlappungsschutz `running`; Systemkontext-Lesen nur ueber `forSystem()` (+ `system_read_policy`). Fuer Etappe 1 genuegt: Status beim Oeffnen/"Aktualisieren" abfragen, optional minuetlicher Cron nur fuer offene Auftraege. |
|
||||
| DTOs | `nextcloud-status/dto/nextcloud-instance.dto.ts` | `class-validator`; globale `ValidationPipe` ist in `main.ts` aktiv. |
|
||||
| DB-Zugriff | `forTenant(this.prisma, tenantId)` aus `prisma/prisma-tenant.extension.ts` | Jeder neue Zugriff in `rls-access-inventory.spec.ts` eintragen, wenn dort Klassifikation verlangt wird. |
|
||||
| Migration | `apps/api/prisma/migrations/20261002130000_handelsware_datev/migration.sql` | Handgeschrieben mit Kopfkommentar; je Tabelle `tenantId`, `ENABLE`+`FORCE ROW LEVEL SECURITY`, `tenant_isolation_policy USING ("tenantId" = current_tenant_id())`; sonst faellt `rls-coverage.spec.ts`. Naechster Zeitstempel > `20261003120000`. |
|
||||
| Registrierungspunkte | `apps/api/src/app.module.ts` (Importliste), `module-manage-handlers.spec.ts` (Pflicht-Test: jede Schreibroute verlangt Manage, Leseroute nicht) | |
|
||||
|
||||
### Frontend: Registrierungspunkte (alle noetig, sonst erscheint das Modul nicht korrekt)
|
||||
- `apps/web/src/lib/module-loader.ts`: Eintrag `'<slug>': { component: dynamic(() => import('@/app/(portal)/modules/<slug>/page'), { ssr: false }) }`.
|
||||
- `apps/web/src/lib/module-identity.ts`: Symbol in `ICONS` (z. B. `domaincheck: 'globe'` existiert; neuer Slug braucht eigenen Eintrag; fehlt er, gilt 'tile').
|
||||
- `apps/web/src/lib/stores/nav-store.ts`: `MODULE_TITLE_KEYS['<slug>'] = '<ns>.title'`.
|
||||
- `apps/web/src/app/(portal)/modules/<slug>/layout.tsx`: `<ModuleAccessGate moduleSlug="<slug>">` und Eintrag in `module-layouts.test.tsx`.
|
||||
- Seite: `PageHeader` aus `@/components/layout/page-header`, `TabBar` aus `@/components/accounting/tab-bar` (Handelsware-Muster: Tabs Kontakte / Domains / Registrieren / Einstellungen), `useCanManageModule(slug)`. `SettingsSection`/`ControlCenterNav` (`components/control-center/`) gehoeren zum Administrationsbereich (`/admin/...`); fuer ein Modul reicht der Einstellungs-Tab nach Handelsware-Muster. [VERIFIED: ControlCenterNav listet nur /settings und /admin Seiten]
|
||||
- API-Client: `apps/web/src/lib/<name>-api.ts` nach `nextcloud-status-api.ts` (`credentials: 'include'`, `NEXT_PUBLIC_API_URL`).
|
||||
- Texte in `apps/web/src/messages/de.json` UND `en.json` (Paritaets-Tests); deutsche Texte mit echten Umlauten, `umlaut-guard.spec.ts` laeuft ueber de.json - neue Woerter ggf. in `umlaut-dictionary.ts`.
|
||||
- Kein Dashboard-Widget in Etappe 1 (`WIDGET_MODULE_SLUGS` unveraendert).
|
||||
|
||||
## Datenmodell-Empfehlung
|
||||
|
||||
Prinzip: AutoDNS bleibt die Quelle der Wahrheit fuer Kontakte und Domains; lokal liegt nur, was AutoDNS nicht kennt oder was Geld-/Idempotenz-Schutz braucht.
|
||||
|
||||
| Tabelle (alle mit `tenantId`, RLS) | Inhalt | Begruendung |
|
||||
|---|---|---|
|
||||
| `DomainsConfig` (Singleton je tenantId) | `environment` (`DEMO`/`LIVE`, Standard `DEMO`), je Umgebung `demoUser`, `demoEncryptedPassword`, `demoContext Int?`, `liveUser`, `liveEncryptedPassword`, `liveContext Int? (Standard 4)`, `defaultNameServers` (Text-Array/Json) | Zwei getrennte Zugangssaetze: Umschalter kann nie versehentlich Live-Zugang in Demo tippen und umgekehrt; Wechsel verliert nichts. Passwort-Spalten `iv:authTag:ciphertext`. |
|
||||
| `DomainsCustomer` | `id`, `name` (je Mandant eindeutig), `isOwnCompany Boolean` | Kunden gibt es im System noch nicht (Nextcloud nutzt nur Freitext `customerName`). Eigene kleine Tabelle erlaubt Gruppieren/Filtern ohne Tippfehler-Duplikate; die eigene Firma ist ein Eintrag mit Markierung. Kein Firmenname als Vorgabe. |
|
||||
| `DomainsContactAssignment` | `environment`, `autodnsContactId Int`, `customerId` (FK `DomainsCustomer`, `onDelete: Restrict`) ; `@@unique([tenantId, environment, autodnsContactId])` | AutoDNS-Kontakt-IDs von Demo und Live kollidieren zwangslaeufig, daher Umgebung im Schluessel. Kontakte selbst werden NICHT gespiegelt. |
|
||||
| `DomainsOrder` | `id`, `environment`, `domainName`, `status` (`DRAFT`, `SUBMITTING`, `SUBMITTED`, `SUCCESS`, `FAILED`, `UNKNOWN`, `CANCELED`), `jobId Int?`, `payload Json` (Kontakt-IDs, Nameserver, Laufzeit, angezeigter Preis/Waehrung), `createdByUserId`, `confirmedByUserId`, `confirmedAt`, `errorText`, `lastCheckedAt` ; Teil-Unique auf offene Bestellungen je (`tenantId`,`environment`,`domainName`) | Idempotenz, Nachverfolgbarkeit, Audit "wer hat Geld ausgegeben". |
|
||||
| Kein lokaler Domain-Cache in Etappe 1 | Domainliste wird live seitenweise gelesen (`view.limit`), `ownerc` per `keys[]` mitgeholt; Kunde der Domain = Kunde des Inhaber-Kontakts (aus `DomainsContactAssignment`) | Immer aktuell, keine Synchronisationsfehler. Bei vielen Hundert Domains oder AutoDNS-Ausfall spaeter Snapshot-Tabelle + Cron nachruesten. Kurzer In-Memory-Zwischenspeicher (z. B. 60 s) pro (tenant, environment) schuetzt das 3-Aufrufe-pro-Sekunde-Limit. |
|
||||
|
||||
"Kunde" einer Domain, die der Inhaber-Kontakt nicht zuordenbar macht (Kontakt ohne Zuordnung): als "Nicht zugeordnet" anzeigen und im Filter anbieten. Spaetere Abweichung (Domain gehoert anderem Kunden als ihr Inhaber) ist eine eigene Zuordnungstabelle in Etappe 2 - Architektur laesst das zu.
|
||||
|
||||
## Architektur-Muster fuer die Registrierung (Geldsicherheit)
|
||||
|
||||
```
|
||||
Browser --1 Pruefen(name,tld)--> API --> POST /domainstudio (WHOIS+PRICE) --> Ergebnis FREE?
|
||||
Browser --2 Entwurf(Kontakte,NS,Laufzeit)--> API: legt DomainsOrder DRAFT an, liefert Zusammenfassung + orderId
|
||||
Browser --3 "Verbindlich registrieren" (orderId, Haken gesetzt)--> API:
|
||||
UPDATE ... SET status='SUBMITTING' WHERE id=? AND status='DRAFT' AND environment=<aktive> (Zaehler == 1 ?)
|
||||
nein -> 409 "Bereits abgeschickt"
|
||||
ja -> POST /domain (EINMAL, KEIN Retry) -> Job-Id speichern, status='SUBMITTED'
|
||||
Netzwerkfehler/Timeout/unlesbare Antwort -> status='UNKNOWN' (nie zurueck auf DRAFT)
|
||||
Browser/Cron --4 Aktualisieren--> GET /job/{id} -> SUCCESS/FAILED/... ; bei UNKNOWN: POST /job/_search bzw. GET /domain/{name} zum Abgleich
|
||||
```
|
||||
|
||||
## Don't Hand-Roll
|
||||
| Problem | Nicht bauen | Stattdessen | Warum |
|
||||
|---|---|---|---|
|
||||
| Verschluesselung | eigenes Krypto | `CryptoService` | AES-256-GCM, einheitlicher Schluessel |
|
||||
| Moduleberechtigung | eigene Rollenpruefung | `@UseModule` + `@ModuleManage` | Gruppen-/Direktfreigaben, Admin-Kurzschluss |
|
||||
| Mandantenschutz | WHERE tenantId von Hand | `forTenant()` + RLS-Migration | RLS-Tests erzwingen es |
|
||||
| HTTP/Timeout | neuer Client-Stil | `undici` wie Proxmox | bewaehrt, testbar |
|
||||
| Domainnamen-Pruefung | Regex-Eigenbau | Pruefung per Muster `^[a-z0-9-]+\.[a-z.]{2,}$` nur als Vorfilter; Gueltigkeit entscheidet DomainStudio-Status `INVALID` | IDN/TLD-Sonderfaelle |
|
||||
|
||||
## Common Pitfalls
|
||||
1. **Timeout bei `POST /domain` ist unbekannter Ausgang.** Nie automatisch wiederholen. Status `UNKNOWN`; Abgleich per `GET /domain/{name}` (existiert -> bestellt) oder `POST /job/_search`; dem Benutzer "Ergebnis ungeklaert, bitte pruefen" zeigen. Kein globaler Retry-Wrapper fuer POST.
|
||||
2. **Doppelklick / zwei Browser-Tabs / Wiederholen nach Fehler.** Schutz nur serverseitig durch atomaren Compare-and-Set in der DB (`updateMany ... where status='DRAFT'`, Zaehler pruefen); Button-Sperre im Browser ist nur Komfort. Eine neue Bestellung derselben Domain erst, wenn die alte `FAILED`/`CANCELED` ist. AutoDNS bietet keinen dokumentierten Idempotenz-Schluessel; Header `X-Domainrobot-Ctid` existiert (js-sdk `Headers.js`), eine serverseitige Dublettenerkennung darauf ist [ASSUMED] - nur zur Korrelation im Log verwenden.
|
||||
3. **Demo/Live verwechselt.** Umgebung ist Teil jeder Bestellung und jeder Zuordnung; beim Bestaetigen muss `order.environment === config.environment` gelten (sonst 409). Deutlicher, dauerhafter Banner "DEMO-System" bzw. "LIVE - kostenpflichtig" in Modulkopf und Bestaetigungsdialog; Live-Registrierung verlangt ausdrueckliches Haeckchen "verbindlich registrieren (kostenpflichtig)". Umgebungswechsel nur mit Verwalten und mit Bestaetigungsdialog; Standard fuer neue Installation: DEMO. Basis-URL ist Aufzaehlung, nie Freitext.
|
||||
4. **Geheimnisse.** Passwort nie in Responses, Logs oder Fehlertexten (Basic-Header, Request-Body-Logging aus; `request-log.ts` protokolliert nur Methode/Pfad/Status). Fehlermeldungen gekuerzt; AutoDNS-`messages[].text` ist unkritisch, aber ohne `stid`-Zwang anzeigen. Entschluesselungsfehler lautstark loggen, nicht als "kein Passwort" werten.
|
||||
5. **NestJS-Routenreihenfolge.** `contacts/import`, `contacts/availability`, `registrations/preview`, `domains/check`, `connection-test` stehen VOR `contacts/:id`, `registrations/:id`; im Controller-Spec die Deklarationsreihenfolge pruefen (Vorbild `handelsware-datev.controller.spec.ts`). Jede neue Verwalten-Route in `module-manage-handlers.spec.ts` eintragen.
|
||||
6. **Rate-Limit 3 Anfragen/s/IP.** Einen Limiter (Warteschlange mit 350 ms Abstand je Prozess) im AutoDNS-Client, Listen in Seiten zu je 100-250; die Domainliste nicht pro Zeile mit `GET /domain/{name}` anreichern.
|
||||
7. **HTTP 200 mit Fachfehler.** `status.type==='ERROR'` immer auswerten (auch `messages[]`), sonst gilt eine fehlgeschlagene Bestellung als erfolgreich.
|
||||
8. **Kontakt-IDs ueber Umgebungen.** Siehe Datenmodell; Zuordnungen nie ohne `environment` auswerten.
|
||||
9. **Kontakte aendern kann Domains beeinflussen** (`confirmOwnerConsent` bei Inhaberwechsel gTLDs). Etappe 1: Kontakte nur anlegen/lesen, nicht aendern.
|
||||
10. **Nameserver-Check .de.** Standard-Nameserver in den Einstellungen muessen vorab bei AutoDNS/Anbieter eingerichtet sein, sonst scheitert `.de` mit `FAILED`.
|
||||
|
||||
## Validation Architecture
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| Framework | Vitest 3.2.6 (apps/api), 4.1.9 (apps/web) [VERIFIED: CLAUDE.md Stack-Tabelle] |
|
||||
| API-Mock | AutoDNS-Client hinter Schnittstelle (`AutodnsClient`), im Test durch Fake ersetzt; HTTP-Schicht mit `undici` `MockAgent` pruefen: Header (`Authorization`, `X-Domainrobot-Context`, `User-Agent`), URL je Umgebung, Timeout |
|
||||
| Quick run | `pnpm --filter api exec vitest run src/autodns-domains` |
|
||||
| Pflicht-Tests | Controller-Reihenfolge; `module-manage-handlers.spec.ts` erweitern; Passwort nie in Response (`'********'`/`hasPassword`); Bestellung: zweiter Bestaetigungsaufruf -> 409 und `POST /domain` genau einmal; Timeout -> `UNKNOWN` ohne Retry; Umgebungswechsel zwischen DRAFT und Bestaetigung -> 409; HTTP 200 + `status.type=ERROR` -> Fehler; `rls-coverage.spec.ts` und RLS-Inventar; de/en-Paritaet der Texte |
|
||||
| Echtprobe | Erst nach Eintrag von Demo-Zugangsdaten durch den User: Verbindungstest, Kontakt anlegen, Verfuegbarkeit, Bestellung, Job-Antwortform (schliesst die [ASSUMED]-Punkte) |
|
||||
|
||||
## Security Domain
|
||||
| ASVS | Gilt | Kontrolle |
|
||||
|---|---|---|
|
||||
| V4 Zugriffskontrolle | ja | `@UseModule` Klasse, `@ModuleManage` auf Registrieren/Kontakt anlegen/Einstellungen/Verbindungstest; `tenantId` nur aus `req.tenantId` |
|
||||
| V5 Eingabevalidierung | ja | `class-validator`-DTOs; Domainname normalisieren (klein, ohne Protokoll/Pfad); Kontakt-/Job-Ids als Integer |
|
||||
| V6 Kryptografie | ja | `CryptoService`, nichts Eigenes |
|
||||
| V9 Kommunikation | ja | nur `https`, feste Hosts (kein SSRF), Zertifikatspruefung an |
|
||||
| V7 Protokollierung | ja | Bestellungen mit Benutzer-Id und Zeitpunkt in `DomainsOrder` (Audit) |
|
||||
|
||||
## Assumptions Log
|
||||
| # | Annahme | Abschnitt | Risiko wenn falsch |
|
||||
|---|---|---|---|
|
||||
| A1 | Demo-Context ist unklar (1 laut Drittanbieter, 4 in SDK-Beispielen) | Basis | Verbindungstest Demo scheitert; deshalb frei eintragbar |
|
||||
| A2 | Pflichtfelder Kontakt (type, Name/Organisation, address[], pcode, city, country, email, phone) | Routen | Demo-Fehler beim Anlegen; Formular nachbessern |
|
||||
| A3 | `POST /domainstudio` mit `initial.tlds`+`searchToken` bzw. `custom.domains` liefert genau die gewuenschte Domain mit WHOIS-Status | Routen | Verfuegbarkeitspruefung anders aufbauen |
|
||||
| A4 | Job-Id in Antwort von `POST /domain` unter `data[0].id` | Routen | Statusnachverfolgung muss Antwortform nachziehen; Fallback `POST /job/_search` |
|
||||
| A5 | `{ "id": n }` genuegt als Kontaktverweis in `ownerc/adminc/techc/zonec` | Routen | ganzes Kontakt-Objekt aus `GET /contact/{id}` mitsenden |
|
||||
| A6 | Dedizierter API-Benutzer ohne 2FA; 2FA-Token-Header fuer JSON unsicher | Basis | Anmeldung scheitert bei 2FA-Benutzer |
|
||||
| A7 | HTTP 200 mit Fachfehler moeglich; 429 bei Rate-Limit | Pitfalls | schlimmstenfalls zu vorsichtige Auswertung |
|
||||
| A8 | DENIC/AutoDNS-.de-Pflichten fuer adminc/techc/zonec und Nameserver-Pruefung | .de | Bestellung scheitert mit klarer API-Meldung |
|
||||
| A9 | Falsche Anmeldungen koennen den Benutzer sperren | Basis | Verbindungstest nur einmal je Klick, nie Schleife |
|
||||
|
||||
## Open Questions
|
||||
1. **Existiert beim User ein dedizierter AutoDNS-API-Benutzer (ohne 2FA) und welcher Context gilt fuer Live/Demo?** Klaeren beim Eintragen der Demo-Zugangsdaten; Einstellungsmaske nimmt beides auf.
|
||||
2. **Preisanzeige in der Zusammenfassung:** `PRICE`-Dienst liefert Betrag/Waehrung; falls er fehlt oder `FAILED` ist, Bestellung nur mit Hinweis "Preis nicht ermittelbar" zulassen, nicht stillschweigend ohne Preis.
|
||||
3. **Taeglicher Abgleich der Auftraege:** Entscheidung Etappe 1 ohne Cron (Abfrage beim Oeffnen/Aktualisieren) oder mit minuetlichem Cron nur fuer offene Auftraege. Empfehlung: Cron nachruesten, sobald die Demo-Antwortform bekannt ist.
|
||||
|
||||
## Sources
|
||||
### Primary (HIGH)
|
||||
- OpenAPI 2.0 `https://raw.githubusercontent.com/InterNetX/domainrobot-api/master/src/domainrobot.json` - Pfade, Schemas, Enums
|
||||
- `https://help.internetx.com/x/cQbj` (JSON API Basics) - URLs live/demo, Limits, `/hello`, Statuscodes S/E/N
|
||||
- InterNetX SDK-Beispiele: `js-domainrobot-sdk` (`examples/contact|domain|domainstudio`, `src/services/DomainService.js`, `src/lib/Headers.js`), `php-domainrobot-sdk` (`example/domain/DomainList.php`), `java-domainrobot-sdk` README (Demo-URL)
|
||||
- Curl-Proben ohne Zugangsdaten gegen beide Hosts am 2026-10-08
|
||||
- Codebase: `apps/api/src/nextcloud-status/*`, `apps/api/src/crypto/crypto.service.ts`, `apps/api/src/ldap/ldap-config.service.ts`, `apps/api/src/module-registry/module.guard.ts`, `apps/api/src/handelsware-datev/*`, `apps/api/prisma/migrations/20261002130000_handelsware_datev`, `apps/web/src/lib/module-loader.ts|module-identity.ts|stores/nav-store.ts`
|
||||
|
||||
### Secondary/Tertiary (MEDIUM/LOW)
|
||||
- docs.wisecp.com/en/internetx.md (Demo-Context 1, Drittanbieter)
|
||||
- DENIC Pressemitteilung 25.05.2018 und Registrar-Wissensdatenbanken (.de-Regeln, ueber Websuche, nicht im Volltext geprueft)
|
||||
|
||||
## Metadata
|
||||
**Confidence:** Standard-Stack HIGH (nichts Neues); AutoDNS-Routen/Schemas HIGH; Antwortformen der Bestell-/Job-Routen MEDIUM; .de-Regeln LOW-MEDIUM
|
||||
**Research date:** 2026-10-08, gueltig ca. 30 Tage (OpenAPI aendert sich selten, aber vor Implementierung gegen Demo gegenpruefen)
|
||||
Reference in New Issue
Block a user