330 lines
43 KiB
Markdown
330 lines
43 KiB
Markdown
---
|
||
phase: quick-261008-h3t
|
||
plan: 01
|
||
type: execute
|
||
wave: 1
|
||
depends_on: []
|
||
quick_id: 261008-h3t
|
||
description: "Domains: Standard-Nameserver aus dem AutoDNS-Benutzerprofil statt aus einer Tessera-Einstellung"
|
||
date: 2026-10-08
|
||
files_modified:
|
||
# Task 1 — tracer (API): AutoDNS profile -> parser -> draft payload -> POST /domain body, plus GET name-servers route
|
||
- apps/api/src/domains/domain-name.ts
|
||
- apps/api/src/domains/autodns-parse.ts
|
||
- apps/api/src/domains/autodns-parse.spec.ts
|
||
- apps/api/src/domains/domains-orders.service.ts
|
||
- apps/api/src/domains/domains-orders.service.spec.ts
|
||
- apps/api/src/domains/dto/domains-order.dto.ts
|
||
- apps/api/src/domains/domains.controller.ts
|
||
- apps/api/src/domains/domains.controller.spec.ts
|
||
- apps/api/src/module-registry/module-manage-handlers.spec.ts
|
||
- apps/api/src/domains/domains-settings.service.ts
|
||
# Task 2 — web: Registrieren shows AutoDNS nameservers read-only, hint and lock when missing
|
||
- apps/web/src/lib/domains-api.ts
|
||
- apps/web/src/app/(portal)/modules/domains/components/RegisterTab.tsx
|
||
- apps/web/src/app/(portal)/modules/domains/components/RegisterTab.test.tsx
|
||
- apps/web/src/messages/de.json
|
||
- apps/web/src/messages/en.json
|
||
# Task 3 — remove the setting everywhere (DB column, API, web), docs, changelog, rebuild
|
||
- apps/api/prisma/schema.prisma
|
||
- apps/api/prisma/migrations/20261008160000_domains_drop_default_nameservers/migration.sql
|
||
- apps/api/src/domains/domains-settings.service.spec.ts
|
||
- apps/api/src/domains/domains.types.ts
|
||
- apps/api/src/domains/dto/domains-settings.dto.ts
|
||
- apps/api/src/domains/dto/domains-settings.dto.spec.ts
|
||
- apps/web/src/app/(portal)/modules/domains/components/SettingsTab.tsx
|
||
- apps/web/src/app/(portal)/modules/domains/domains-page.test.tsx
|
||
- CHANGELOG.md
|
||
- docs/anleitung-anwender.md
|
||
- docs/anleitung-administration.md
|
||
- docs/mandantentrennung-zugriffsklassifikation.md
|
||
autonomous: true
|
||
requirements: [QUICK-261008-h3t]
|
||
|
||
estimate:
|
||
tokens: 100000
|
||
raw_tokens: 100000
|
||
tasks: 3
|
||
confidence: low
|
||
|
||
must_haves:
|
||
truths:
|
||
- "The Einstellungen tab of the module Domains shows no 'Standard-Nameserver' card any more; GET/PUT settings and GET status carry no nameserver field; after the migration the table DomainsConfig has no nameserver column (D-01)"
|
||
- "A manager opening the Registrieren tab sees the standard nameservers that AutoDNS holds in the profile of the configured AutoDNS user (GET /user/{user}/{context}/profile of the active system), read-only, in AutoDNS order (by the number in the profile key), with no input, add or remove controls (D-02, D-03)"
|
||
- "Creating a draft ignores any nameservers sent by the browser, reads the AutoDNS profile on the server, stores exactly that ordered list in the draft payload and returns it in the summary; confirming sends exactly that stored list in that order in the one POST /domain; the WR-02 version binding, the atomic DRAFT-to-SUBMITTING claim, the single POST without retry and the UNKNOWN handling are unchanged (D-03)"
|
||
- "If AutoDNS cannot be read or the profile yields fewer than two recognisable nameservers (or two different values for the same number), the Registrieren tab shows a German Sie-form hint ('In AutoDNS sind keine Standard-Nameserver hinterlegt …' respectively 'Tessera konnte die Standard-Nameserver nicht aus AutoDNS lesen …'), 'Zusammenfassung anzeigen' stays disabled, the server refuses the draft with 409 noDefaultNameServers or the AutoDNS error without writing a row, and submit refuses a draft with fewer than two nameservers before the claim — nothing is guessed (D-04)"
|
||
- "docs/anleitung-anwender.md, docs/anleitung-administration.md and the existing Domains lines under 'Unveröffentlicht' in CHANGELOG.md describe nameservers coming from AutoDNS; the 261008-dts SUMMARY is unchanged (D-05)"
|
||
- "The full api and web test suites, both tsc runs and the de/en key parity stay green; the rebuilt stack answers GET modules/domains/name-servers with 200, 409, 502 or 504 (never 404 or 500)"
|
||
artifacts:
|
||
- path: "apps/api/src/domains/autodns-parse.ts"
|
||
provides: "parseProfileNameServers: tolerant recognition of the standard nameservers in an AutoDNS user profile ([ASSUMED] key names)"
|
||
exports: ["parseProfileNameServers"]
|
||
- path: "apps/api/src/domains/domains-orders.service.ts"
|
||
provides: "getProfileNameServers, profile read inside createOrder, pre-claim guard for drafts without nameservers"
|
||
contains: "noDefaultNameServers"
|
||
- path: "apps/api/prisma/migrations/20261008160000_domains_drop_default_nameservers/migration.sql"
|
||
provides: "drops the obsolete settings column"
|
||
contains: "DROP COLUMN"
|
||
- path: "apps/web/src/app/(portal)/modules/domains/components/RegisterTab.tsx"
|
||
provides: "read-only AutoDNS nameserver list, hint with retry, summary locked without nameservers"
|
||
key_links:
|
||
- from: "apps/api/src/domains/domains-orders.service.ts createOrder"
|
||
to: "AutoDNS GET /user/{user}/{context}/profile"
|
||
via: "readProfileNameServers(credentials) -> payload.nameServers"
|
||
pattern: "readProfileNameServers\\(credentials\\)"
|
||
- from: "apps/api/src/domains/domains-orders.service.ts submitOrder"
|
||
to: "AutoDNS POST /domain"
|
||
via: "stored payload list in stored order"
|
||
pattern: "payload\\.nameServers\\.map"
|
||
- from: "apps/api/src/domains/domains.controller.ts"
|
||
to: "DomainsOrdersService.getProfileNameServers"
|
||
via: "GET name-servers with ModuleManage('domains')"
|
||
pattern: "@Get\\('name-servers'\\)"
|
||
- from: "apps/web/src/app/(portal)/modules/domains/components/RegisterTab.tsx"
|
||
to: "GET /modules/domains/name-servers"
|
||
via: "getNameServers() in apps/web/src/lib/domains-api.ts"
|
||
pattern: "getNameServers\\("
|
||
---
|
||
|
||
<objective>
|
||
The Tessera setting "Standard-Nameserver" in the module Domains (built in quick 261008-dts, commits 1f1c984..53b73dd) is wrong and goes away. AutoDNS already holds the standard nameservers in the profile of the AutoDNS user (for this user: ns2.ctl.de, b.ns14.net, c.ns14.net, d.ns14.net in exactly that order). Tessera reads them from AutoDNS when a registration is prepared, shows them for control only, stores them with the draft the user confirms and sends them explicitly, in AutoDNS order, in the single POST /domain. Company-specific values are never hard-coded in Tessera — not in code, tests, defaults or docs.
|
||
|
||
Locked requirements from the request (cited below as D-xx; numbering follows the request):
|
||
- D-01 Remove the settings card "Standard-Nameserver" (web SettingsTab, API DTO/service, validation). DB column: removed by migration (chosen in Task 3; values are worthless).
|
||
- D-02 On registration, read the standard nameservers from the AutoDNS user profile (GET /user/{name}/{context}/profile, response UserProfileViews with profiles[] of key/value). The key names are UNKNOWN: recognise tolerantly (keys containing "ns" plus a number, sorted by that number), encapsulated in one function with tests, assumption documented as [ASSUMED] and put on the demo checklist.
|
||
- D-03 Registrieren tab and summary show the AutoDNS nameservers read-only, in AutoDNS order. They are stored with the draft (part of what the user confirms; WR-02 version binding stays intact) and sent explicitly in that order in POST /domain.
|
||
- D-04 If Tessera cannot read nameservers from AutoDNS (error or no matching keys): clear German Sie-form hint starting "In AutoDNS sind keine Standard-Nameserver hinterlegt …" and registration locked — nothing guessed.
|
||
- D-05 Update docs/anleitung-anwender.md, docs/anleitung-administration.md and the existing Domains lines under "Unveröffentlicht" in CHANGELOG.md (adapt, no new line). Do NOT change the 261008-dts SUMMARY.
|
||
- Money safety (atomic claim, exactly one POST, UNKNOWN) is not touched except where needed; all existing tests stay green.
|
||
|
||
Discretion choices (documented here, cited in tasks):
|
||
- On a read ERROR (auth, timeout, gateway) the hint says "Tessera konnte die Standard-Nameserver nicht aus AutoDNS lesen …" plus the AutoDNS detail, because "no nameservers stored" would be false in that case; both cases lock registration identically. The "no matching keys" case uses the requested opening sentence verbatim.
|
||
- The server reads the profile itself inside createOrder (authoritative); the browser list is informational. The client never sends nameservers.
|
||
- Fewer than two recognised nameservers counts as "not stored" (.de needs at least two). No upper cap and no truncation (truncating would be guessing); AutoDNS validates the rest.
|
||
|
||
Purpose: AutoDNS stays the single source of the nameserver defaults; Tessera holds no company-specific values and never registers with nameservers the user did not see.
|
||
Output: profile parser with tests, API route GET name-servers, draft/summary/submit using the AutoDNS list, read-only Registrieren display with lock, setting removed including DB column, docs and changelog updated.
|
||
</objective>
|
||
|
||
<execution_context>
|
||
@~/.claude/gsd-core/workflows/execute-plan.md
|
||
@~/.claude/gsd-core/templates/summary.md
|
||
</execution_context>
|
||
|
||
<context>
|
||
@.planning/STATE.md
|
||
@./CLAUDE.md
|
||
@.planning/quick/261008-dts-modul-domains-autodns-anbindung-kontakte/261008-dts-SUMMARY.md
|
||
@.planning/quick/261008-dts-modul-domains-autodns-anbindung-kontakte/261008-dts-REVIEW.md
|
||
|
||
Project rules that apply here:
|
||
- NestJS: static routes before any `:id` route (domains.controller.spec.ts checks declaration order).
|
||
- API TS lib has no Object.hasOwn; use `Object.prototype.hasOwnProperty.call` or `in`.
|
||
- Never read .env files. Rebuild locally with `docker compose up -d --build api web` (the api container runs `prisma migrate deploy` on start). No deploy to the test server, no push (commits stay local; the user pushes bundled).
|
||
- UI texts: Sie-form, real umlauts (apps/web/src/messages/umlaut-guard.spec.ts fails on ae/oe/ue substitutes), de/en keys identical, no "Mandant"/"Lizenz".
|
||
- Global ValidationPipe: `whitelist: true`, no forbidNonWhitelisted — unknown body fields are stripped, not rejected.
|
||
|
||
AutoDNS spec facts (Swagger 2.0, already checked by the planner; the local copy is a session scratch file the executor need not open):
|
||
- GET /user/{name}/{context}/profile (operationId userProfileInfo, task 1301017) -> JsonResponseDataUserProfileViews: `data` is an array of UserProfileViews `{ profiles: UserProfileView[] }`; UserProfileView has `key` (string, example "techc"), `value` (string), `flag`, `inherited` (bool), `readonly` (bool), created/updated/owner/updater.
|
||
- Domain has `nameServers[]` (objects with `name`), `nameServerGroup`, `zone`. The existing POST /domain body already sends `nameServers: [{ name }]`.
|
||
- Base URLs (autodns-client.ts AUTODNS_BASE_URLS): Demo `https://api.demo.autodns.com/v1`, Live `https://api.autodns.com/v1`. `autodnsRequest` rejects paths containing `..`, `?`, `#`, `//` or whitespace by throwing.
|
||
</context>
|
||
|
||
<tasks>
|
||
|
||
<task type="tracer" tdd="true">
|
||
<name>Task 1 (tracer): AutoDNS profile -> parser -> draft payload -> POST /domain body, plus GET name-servers</name>
|
||
<files>apps/api/src/domains/domain-name.ts, apps/api/src/domains/autodns-parse.ts, apps/api/src/domains/autodns-parse.spec.ts, apps/api/src/domains/domains-orders.service.ts, apps/api/src/domains/domains-orders.service.spec.ts, apps/api/src/domains/dto/domains-order.dto.ts, apps/api/src/domains/domains.controller.ts, apps/api/src/domains/domains.controller.spec.ts, apps/api/src/module-registry/module-manage-handlers.spec.ts, apps/api/src/domains/domains-settings.service.ts</files>
|
||
<read_first>apps/api/src/domains/autodns-parse.ts, apps/api/src/domains/domains-orders.service.ts (lines 1-600: ERR, readPayload, callRaw, createOrder, submitOrder), apps/api/src/domains/domains-orders.service.spec.ts (harness lines 1-160, createOrder block around line 333, submit block around line 480-560), apps/api/src/domains/domains.controller.ts, apps/api/src/domains/domains.controller.spec.ts (lines 1-60 and 95-140), apps/api/src/module-registry/module-manage-handlers.spec.ts (lines 85-105), apps/api/src/domains/domain-name.ts</read_first>
|
||
<behavior>
|
||
- parseProfileNameServers, UserProfileViews shape: entries ns3/ns1/ns4/ns2 listed out of order (plus a techc entry) -> hostnames ordered ns1, ns2, ns3, ns4; techc ignored (D-02)
|
||
- numeric sort: ns10 comes after ns9, not after ns1
|
||
- value normalisation: " Z.Example.NET. " -> "z.example.net"; values that are no hostname (IP 192.0.2.1, empty, "kein rechner", "a.example.org 192.0.2.1") are skipped
|
||
- key variants recognised: nameserver1, NS_2, default_ns3, nserver4 (case-insensitive)
|
||
- flat items `{ key, value }` directly in data are accepted as well as `{ profiles: [...] }` and a single object with profiles
|
||
- same number with two different hostnames -> empty list (ambiguous, nothing guessed); same number with the same hostname -> once; the same hostname under two numbers -> first kept
|
||
- list fallback, only when no numbered key yields a hostname: key "nameservers" with "a.example.org, b.example.org" -> that order; two list keys with different lists -> empty
|
||
- garbage input (null, "x", {}, [1, 2]) -> `{ nameServers: [], keys: [] }`; `keys` lists the profile keys seen (for the log line)
|
||
- getProfileNameServers: exactly one GET to `https://api.demo.autodns.com/v1/user/api-user/4/profile`; returns `{ environment: 'DEMO', nameServers }` in key order; a user name with a space is percent-encoded in the path
|
||
- profile without nameserver keys -> 409 code noDefaultNameServers; AutoDNS 401 -> 502 autodnsAuth; timeout -> 504 (D-04)
|
||
- createOrder: payload.nameServers and summary.nameServers equal the profile order (neither alphabetical nor from the request); a request body with nameServers ['evil.example.com', 'x.example.com'] is ignored (D-03)
|
||
- createOrder with a profile without nameservers -> 409 noDefaultNameServers, no row written, no /domainstudio call; profile answered 500 -> rejected, no row (D-04)
|
||
- createOrder on an existing DRAFT re-reads the profile and returns a new version (WR-02 binding intact)
|
||
- tracer chain (describe 'Nameserver aus AutoDNS (h3t)'): createOrder -> submitOrder(id, version) -> exactly one POST /domain whose body.nameServers is [{ name }] in profile order
|
||
- submit guard: a DRAFT whose payload.nameServers is [] or has one entry -> 400 orderInvalid, the claim updateMany is not called, zero AutoDNS calls
|
||
- every existing submit, claim, UNKNOWN, reconcile and cancel test passes unchanged
|
||
- controller: getNameServers is GET 'name-servers', carries ModuleManage('domains'), no role decorator, is declared before every :id handler and forwards req.tenantId
|
||
</behavior>
|
||
<action>
|
||
Write the tests from the behavior list first (RED), then implement (GREEN). This task proves the money path end-to-end inside the API; the web follows in Task 2, the removal of the old setting in Task 3.
|
||
|
||
1. domain-name.ts: move the hostname regex constant HOSTNAME_PATTERN here unchanged and export it. In domains-settings.service.ts delete its local definition and import it from './domain-name' (its own nameserver normaliser stays until Task 3). In domains-orders.service.ts import it from './domain-name' instead of the settings service. autodns-parse.ts imports it from './domain-name' too (no dependency from the parser on a Nest service).
|
||
|
||
2. autodns-parse.ts (D-02): add exported parseProfileNameServers(data: unknown) returning `{ nameServers: string[]; keys: string[] }`. Doc comment in German stating: the key names of the standard nameservers in the AutoDNS user profile are not documented — [ASSUMED], recognised tolerantly, checked by H-1 on the demo checklist of quick 261008-h3t. Rules:
|
||
a. Collect entries: data may be an array of `{ profiles: [...] }` (spec), an array of flat `{ key, value }` items, or one object with `profiles`. Keep items whose key and value are strings. `keys` = distinct trimmed keys in first-seen order, at most 50, each cut to 60 characters.
|
||
b. Normalise a value: trim, lowercase, remove one trailing dot; accept it only if the whole result matches HOSTNAME_PATTERN (no token splitting for numbered keys).
|
||
c. Numbered keys: lowercase the key and search anywhere in it for the regex `(?:nameserver|name[_-]server|nserver|ns)[_.-]?(\d{1,2})(?!\d)`; the captured number is the position. Skip entries whose value is not a hostname (for example glue addresses).
|
||
d. If one position carries two different hostnames, return an empty list (never pick one). The same hostname twice at one position counts once.
|
||
e. Sort by position numerically, then drop repeated hostnames keeping the first occurrence.
|
||
f. Only if step c produced no hostname: keys matching exactly `^(?:default[_.-]?)?(?:nameservers?|name[_-]servers?|nservers?|ns)$` whose value split on `[\s,;]+` gives tokens that are ALL hostnames provide that list in written order; two such keys with different lists give an empty list.
|
||
g. No minimum here; the caller enforces it.
|
||
|
||
3. domains-orders.service.ts:
|
||
- Add ERR.noDefaultNameServers with code 'noDefaultNameServers' and message "In AutoDNS sind keine Standard-Nameserver hinterlegt. Bitte hinterlegen Sie mindestens zwei Nameserver als Standard in AutoDNS; bis dahin ist keine Registrierung möglich." (thrown as ConflictException, D-04).
|
||
- Private readProfileNameServers(credentials): exactly one callRaw GET to the path `/user/` + encodeURIComponent(credentials.user) + `/` + credentials.context + `/profile` — no query, no retry. A thrown error (for example the client's path check) becomes BadGatewayException with code 'autodnsError' and message "Tessera konnte die Standard-Nameserver nicht aus AutoDNS lesen."; a result with ok false is thrown via autodnsFailureToHttp. Parse with parseProfileNameServers; with fewer than MIN_NAMESERVERS (2) entries log one warning via this.logger.warn that starts with "Keine Standard-Nameserver im AutoDNS-Profil erkannt" and lists only the profile key names (never values, never credentials), then throw ERR.noDefaultNameServers. Otherwise return the list.
|
||
- Public getProfileNameServers(tenantId): getActiveCredentials (409 notConfigured unchanged), then readProfileNameServers; returns `{ environment, nameServers }`.
|
||
- createOrder (D-03): delete the use of the request's nameservers and the method normalizeNameServers plus MAX_NAMESERVERS (no other user). Directly after the existing-order check and before availabilityFor, call readProfileNameServers(credentials) and put the returned ordered list into payload.nameServers. Everything else (availability, contact check, write, summary with version) stays as is; the summary keeps returning payload.nameServers.
|
||
- submitOrder: the only change is the existing pre-claim guard — "payload missing" becomes "payload missing OR payload.nameServers.length below MIN_NAMESERVERS", same 400 orderInvalid. Do not touch the claim, the single POST, the body mapping (order preserved, never sorted), outcomeOfSubmit, recoverFailedOutcomeWrite, reconcile or cancelOrder.
|
||
- Adjust doc comments that mention nameservers coming from the settings or the browser.
|
||
|
||
4. dto/domains-order.dto.ts: remove the nameServers field from CreateDomainsOrderDto and the class-validator imports that become unused; doc comment: the nameservers come from AutoDNS, never from the browser. A browser still sending the field is stripped by the whitelist ValidationPipe.
|
||
|
||
5. domains.controller.ts: add handler getNameServers with `@Get('name-servers')` and `@ModuleManage('domains')`, calling this.orders.getProfileNameServers(this.requireTenantId(req)). Place it directly after checkAvailability in the static section (before every `:id` route). No role decorator. Add "Standard-Nameserver aus AutoDNS lesen" to the Verwalten list in the class doc comment.
|
||
|
||
6. Tests:
|
||
- autodns-parse.spec.ts: the parser cases from the behavior list, example hostnames only (example.org, example.net, example.com).
|
||
- domains-orders.service.spec.ts: extend makeHarness so GET calls whose URL ends with '/profile' are answered by a separate, overridable profile responder (default: envelope with one `{ profiles: [...] }` item listing ns3, ns1, ns4, ns2 out of order with example hostnames whose alphabetical order differs from the key order, plus a techc entry); these calls are still recorded in h.calls. Remove nameServers from the createOrder dto fixture and delete the "Nameserver %j -> BadRequest %s" table test (its rules are gone with D-02). Add the service, createOrder, tracer-chain and submit-guard tests from the behavior list. If an existing test counts all calls of a createOrder run, account for the one profile call explicitly rather than weakening the assertion.
|
||
- domains.controller.spec.ts: add 'getNameServers' to MANAGE_HANDLERS, `getProfileNameServers` to makeOrders, the route expectation `[0, 'name-servers']`, and a forwarding test.
|
||
- module-manage-handlers.spec.ts: add 'getNameServers' to the DomainsController list only; do not reformat the file (its organizeImports finding is pre-existing).
|
||
|
||
Commit: `feat(domains): Standard-Nameserver aus dem AutoDNS-Profil lesen (h3t)`.
|
||
</action>
|
||
<verify>
|
||
<automated>pnpm --filter @tessera/api exec vitest run src/domains module-manage-handlers rls-coverage rls-access-inventory && pnpm --filter @tessera/api exec tsc --noEmit && test -z "$(grep -nE '^\s*@Roles\(' apps/api/src/domains/domains.controller.ts)" && grep -q "@Get('name-servers')" apps/api/src/domains/domains.controller.ts && grep -q "Nameserver aus AutoDNS (h3t)" apps/api/src/domains/domains-orders.service.spec.ts && echo "tracer api ok"</automated>
|
||
</verify>
|
||
<done>Profile parser covered by tests; createOrder stores the AutoDNS list in AutoDNS order and the chained test proves the one POST /domain carries it unchanged; missing or unreadable profile nameservers block the draft with 409/502/504 and no row; drafts without two nameservers never reach the claim; GET name-servers is a Verwalten route before all :id routes; every existing domains test is green.</done>
|
||
</task>
|
||
|
||
<task type="auto" tdd="true">
|
||
<name>Task 2: Registrieren shows the AutoDNS nameservers read-only, with hint and lock when missing</name>
|
||
<files>apps/web/src/lib/domains-api.ts, apps/web/src/app/(portal)/modules/domains/components/RegisterTab.tsx, apps/web/src/app/(portal)/modules/domains/components/RegisterTab.test.tsx, apps/web/src/messages/de.json, apps/web/src/messages/en.json</files>
|
||
<read_first>apps/web/src/app/(portal)/modules/domains/components/RegisterTab.tsx, apps/web/src/app/(portal)/modules/domains/components/RegisterTab.test.tsx, apps/web/src/lib/domains-api.ts (lines 1-80 and 280-360), apps/web/src/messages/de.json and en.json (block domains.register)</read_first>
|
||
<behavior>
|
||
- With getNameServers resolving ['z.example.net', 'b.example.org', 'a.example.org'], the nameserver section lists exactly these in this DOM order, read-only: no textbox labelled 'Nameserver 1', no button 'Nameserver hinzufügen' (D-03)
|
||
- getNameServers rejecting with DomainsRequestError(409, 'noDefaultNameServers', …) shows an alert containing "In AutoDNS sind keine Standard-Nameserver hinterlegt"; after a free domain and chosen contacts, "Zusammenfassung anzeigen" is disabled and createOrder is never called (D-04)
|
||
- getNameServers rejecting with DomainsRequestError(502, 'autodnsAuth', 'Anmeldung bei AutoDNS fehlgeschlagen …') shows "Tessera konnte die Standard-Nameserver nicht aus AutoDNS lesen" plus the server text; "Erneut aus AutoDNS lesen" calls getNameServers again, and after success the list appears and the summary button becomes enabled
|
||
- createOrder is called with exactly { domain, ownerContactId, adminContactId, techContactId, zoneContactId } — no nameServers key (D-03)
|
||
- the summary shows the server's list in its order: "z.example.net, b.example.org, a.example.org"
|
||
- existing tests for double-click lock, submit error lock, orderChanged and cancel stay green
|
||
</behavior>
|
||
<action>
|
||
1. domains-api.ts: add interface DomainsNameServers `{ environment: DomainsEnvironment; nameServers: string[] }` and function getNameServers() that requests '/name-servers' (GET, same request helper as listOrders). Remove nameServers from CreateOrderInput (D-03: the server reads them itself). Leave the DomainsStatus and DomainsSettings types alone — Task 3 removes their old field.
|
||
|
||
2. RegisterTab.tsx (D-03, D-04):
|
||
- Remove the editable nameserver list completely: the MIN/MAX constants used only for it, initialNameServers, the nameServers state seeded from the status prop, the inputs and the add/remove buttons. resetAll no longer touches nameservers.
|
||
- New state for the AutoDNS list, a small union: loading, ok with list, missing, unreadable with detail. Load via getNameServers on mount and whenever status.environment changes, guarded by an alive flag like the contacts effect. DomainsRequestError with code 'noDefaultNameServers' -> missing; any other error -> unreadable with detail = the DomainsRequestError message, else tc('requestFailed').
|
||
- When missing or unreadable, render one hint with role="alert" at the top of the input view (above the domain section, so it is visible before anything is filled in): t('nameServersMissing') or t('nameServersUnreadable') followed by the detail line, plus a SECONDARY_BUTTON t('nameServersRetry') that reloads. The availability check stays usable.
|
||
- The nameserver section keeps its place (shown once the domain is free) and keeps the "Zusammenfassung anzeigen" footer; description t('nameServersDescription'); content: loading -> t('nameServersLoading'); ok -> an ordered list (ol) whose items show t('nameServer', { number }) and the hostname as plain text, nothing editable; missing or unreadable -> t('nameServersBlocked').
|
||
- canSummarize additionally requires the ok state with at least two entries.
|
||
- onSummary calls createOrder without nameservers.
|
||
- The summary row keeps summary.nameServers joined with ", " (server order = AutoDNS order). Update the component doc comment (nameservers come from AutoDNS, read-only).
|
||
|
||
3. de.json / en.json, block domains.register (identical keys in both; Sie-form; real umlauts):
|
||
- intro: "Prüfen Sie, ob eine Domain frei ist, wählen Sie die Kontakte und registrieren Sie die Domain verbindlich bei AutoDNS. Die Nameserver übernimmt Tessera aus AutoDNS."
|
||
- nameServersDescription: "Diese Standard-Nameserver sind in AutoDNS hinterlegt. Tessera verwendet sie unverändert und in dieser Reihenfolge; ändern lassen sie sich nur in AutoDNS."
|
||
- keep nameServer ("Nameserver {number}"); delete addNameServer and removeNameServer
|
||
- add nameServersLoading: "Nameserver werden aus AutoDNS gelesen …"
|
||
- add nameServersMissing: "In AutoDNS sind keine Standard-Nameserver hinterlegt. Bitte hinterlegen Sie mindestens zwei Nameserver als Standard in AutoDNS. Bis dahin ist keine Registrierung möglich."
|
||
- add nameServersUnreadable: "Tessera konnte die Standard-Nameserver nicht aus AutoDNS lesen. Bis das gelingt, ist keine Registrierung möglich." (discretion choice, see objective)
|
||
- add nameServersRetry: "Erneut aus AutoDNS lesen"
|
||
- add nameServersBlocked: "Ohne Standard-Nameserver aus AutoDNS ist keine Registrierung möglich – siehe Hinweis oben."
|
||
- English counterparts with the same meaning. Do not touch domains.settings.nameServers yet (Task 3).
|
||
|
||
4. RegisterTab.test.tsx: add a mockNameServers for getNameServers in the existing vi.mock (default resolves the example list from the behavior block); replace the prefill assertion and the "zwischen 2 und 6" test with the behavior cases; keep the status fixture as it is (Task 3 drops its old field).
|
||
|
||
Commit: `feat(domains): Registrieren zeigt Nameserver aus AutoDNS nur zur Kontrolle (h3t)`.
|
||
</action>
|
||
<verify>
|
||
<automated>pnpm --filter @tessera/web exec vitest run modules/domains src/messages && pnpm --filter @tessera/web exec tsc --noEmit && node -e 'const de=require("./apps/web/src/messages/de.json"),en=require("./apps/web/src/messages/en.json");const w=(o,p,r)=>{for(const[k,v]of Object.entries(o||{})){const q=p+"."+k;if(v&&typeof v==="object")w(v,q,r);else r[q]=v}return r};const a=w(de.domains,"domains",{}),b=w(en.domains,"domains",{});if(Object.keys(a).length<40||Object.keys(a).sort().join()!==Object.keys(b).sort().join()){console.error("key mismatch");process.exit(1)}if(!String(a["domains.register.nameServersMissing"]).startsWith("In AutoDNS sind keine Standard-Nameserver hinterlegt")){console.error("hint text");process.exit(1)}for(const v of [...Object.values(a),...Object.values(b)])if(/mandant|tenant|lizenz|licens/i.test(String(v))){console.error("bad text",v);process.exit(1)}console.log("web register ok")'</automated>
|
||
</verify>
|
||
<done>The Registrieren tab lists the AutoDNS nameservers read-only in AutoDNS order, shows the German hint with a retry button when they are missing or unreadable, keeps "Zusammenfassung anzeigen" disabled in that case, and no longer sends nameservers when creating a draft; de/en keys match; web domains tests and the umlaut guard are green.</done>
|
||
</task>
|
||
|
||
<task type="auto">
|
||
<name>Task 3: Remove the setting everywhere (DB column, API, web), update docs and changelog, rebuild</name>
|
||
<files>apps/api/prisma/schema.prisma, apps/api/prisma/migrations/20261008160000_domains_drop_default_nameservers/migration.sql, apps/api/src/domains/domains-settings.service.ts, apps/api/src/domains/domains-settings.service.spec.ts, apps/api/src/domains/domains.types.ts, apps/api/src/domains/dto/domains-settings.dto.ts, apps/api/src/domains/dto/domains-settings.dto.spec.ts, apps/web/src/lib/domains-api.ts, apps/web/src/app/(portal)/modules/domains/components/SettingsTab.tsx, apps/web/src/app/(portal)/modules/domains/domains-page.test.tsx, apps/web/src/app/(portal)/modules/domains/components/RegisterTab.test.tsx, apps/web/src/messages/de.json, apps/web/src/messages/en.json, CHANGELOG.md, docs/anleitung-anwender.md, docs/anleitung-administration.md, docs/mandantentrennung-zugriffsklassifikation.md</files>
|
||
<read_first>apps/api/src/domains/domains-settings.service.ts, apps/api/src/domains/domains-settings.service.spec.ts, apps/api/src/domains/dto/domains-settings.dto.ts, apps/api/src/domains/dto/domains-settings.dto.spec.ts, apps/web/src/app/(portal)/modules/domains/components/SettingsTab.tsx (lines 1-40 and 340-509), apps/web/src/app/(portal)/modules/domains/domains-page.test.tsx (fixtures near lines 70-90, nameserver test near line 357), CHANGELOG.md lines 1-15, docs/anleitung-administration.md lines 362-372, docs/anleitung-anwender.md lines 174-180, docs/mandantentrennung-zugriffsklassifikation.md line 901</read_first>
|
||
<precondition>The local stack is up: `docker compose ps --status running --services` lists db (the rebuild below needs it).</precondition>
|
||
<reversibility rating="costly">Dropping the column discards the stored values (the user confirmed they are worthless); bringing it back would need a new migration.</reversibility>
|
||
<!-- planner-discipline-allow: defaultNameServers -->
|
||
<action>
|
||
1. Database (D-01): remove the field defaultNameServers from model DomainsConfig in schema.prisma. Create apps/api/prisma/migrations/20261008160000_domains_drop_default_nameservers/migration.sql with a short German header comment (quick-261008-h3t: die Standard-Nameserver kommen aus dem AutoDNS-Benutzerprofil; die Spalte wird nicht mehr gelesen, ihre Werte sind wertlos) and the single statement ALTER TABLE "DomainsConfig" DROP COLUMN "defaultNameServers"; — removal chosen over leaving the column unused because the module is unreleased, nothing reads the column after this task, and a dead column would mislead later work. No RLS change (no new table). Run `pnpm --filter @tessera/api exec prisma generate`.
|
||
|
||
2. API (D-01):
|
||
- domains-settings.service.ts: remove the nameserver constants, the HOSTNAME_PATTERN import, the field from ConfigRow, normalizeNameServers, the saveSettings branch, getDefaultNameServers (no caller since Task 1) and the field in toSettingsView and getStatus; update the class doc comment (no Standard-Nameserver any more). Keep the remaining two forTenant raw hits (loadRow, saveSettings) unchanged so the access inventory stays correct.
|
||
- domains.types.ts: remove the field from DomainsSettingsView and DomainsStatusView.
|
||
- dto/domains-settings.dto.ts: remove the field and the class-validator imports that become unused.
|
||
- Specs: domains-settings.service.spec.ts drops the fixture fields and the nameserver test and adjusts view expectations; add one assertion that getSettings and getStatus return objects without any nameserver property. dto spec: remove the field from the valid body and from the null list.
|
||
|
||
3. Web (D-01):
|
||
- domains-api.ts: remove the field from DomainsStatus, DomainsSettings and SaveDomainsSettingsInput.
|
||
- SettingsTab.tsx: delete NameServersCard, padNameServers, the row constants, its render line and imports that become unused.
|
||
- de.json and en.json: delete the block domains.settings.nameServers in both.
|
||
- domains-page.test.tsx: drop the fixture fields and the test "Nameserver: speichert die bereinigte Liste"; add a test that the Einstellungen tab renders no "Standard-Nameserver" heading and no element with id domains-nameservers.
|
||
- RegisterTab.test.tsx: drop the old field from the status fixture.
|
||
|
||
4. Docs and changelog (D-05; Sie-form in user docs as before; never name the user's concrete nameserver hostnames anywhere):
|
||
- docs/anleitung-administration.md, section "Domains: AutoDNS anbinden": replace the bullet "Standard-Nameserver" with a bullet "Nameserver kommen aus AutoDNS": Tessera has no own nameserver setting; for every registration it reads the standard nameservers from the AutoDNS user profile of the AutoDNS user entered in the settings (also values inherited from a parent user) and uses them unchanged, in the order stored there; store at least two there; they must be set up, because for .de domains the DENIC checks them at registration; if Tessera finds none or cannot read them, the Registrieren tab shows a hint and registration is not possible. In the bullet "Eigener AutoDNS-Benutzer" add that the user must be able to read its own user profile.
|
||
- docs/anleitung-anwender.md, "Eine Domain registrieren" step 2: replace the sentence about prefilled nameservers ("zwei bis sechs") with: below the contacts you see, for control only, the nameservers stored as standard in AutoDNS, in the order stored there; they can only be changed in AutoDNS; if none are stored, a hint appears and registration is locked. Step 3 (summary lists Nameserver) stays.
|
||
- CHANGELOG.md under "Unveröffentlicht", adapt the existing Domains lines, no new line: in the line "Domains, Anbindung an AutoDNS" delete the closing sentence about Standard-Nameserver being prefilled; in the line "Domains, Registrieren" replace "Kontakte und Nameserver wählen" with "Kontakte wählen; die Nameserver übernimmt Tessera unverändert und in derselben Reihenfolge aus den Standardwerten in AutoDNS (sind dort keine hinterlegt, ist die Registrierung gesperrt)".
|
||
- docs/mandantentrennung-zugriffsklassifikation.md line 901 (row domains-settings.service.ts): remove ", Standard-Nameserver" from the description and append "Spalte für Standard-Nameserver mit quick-261008-h3t entfernt (Migration 20261008160000)." Keep the table columns unchanged.
|
||
- Do not edit .planning/quick/261008-dts-*/261008-dts-SUMMARY.md.
|
||
|
||
5. Run `pnpm exec biome check` on the touched source directories apps/api/src/domains and "apps/web/src/app/(portal)/modules/domains" plus apps/web/src/lib/domains-api.ts (not on module-manage-handlers.spec.ts, whose finding is pre-existing) and fix what it reports.
|
||
|
||
6. Rebuild locally: `docker compose up -d --build api web`; wait until the api answers on http://localhost:3001 and its log shows the migrations applied without error. Nothing is pushed and nothing is deployed to the test server.
|
||
|
||
Commit: `refactor(domains): Einstellung Standard-Nameserver entfernt, Doku angepasst (h3t)`.
|
||
</action>
|
||
<verify>
|
||
<automated>pnpm --filter @tessera/api test && pnpm --filter @tessera/web test && pnpm --filter @tessera/api exec tsc --noEmit && pnpm --filter @tessera/web exec tsc --noEmit && ! grep -rn defaultNameServers apps/api/src apps/web/src && ! grep -rnE "ns14|ns2\.ctl" apps/api/src apps/web/src docs CHANGELOG.md && grep -q 'DROP COLUMN "defaultNameServers"' apps/api/prisma/migrations/20261008160000_domains_drop_default_nameservers/migration.sql && grep -q "Nameserver kommen aus AutoDNS" docs/anleitung-administration.md && grep -q "Standardwerten in AutoDNS" CHANGELOG.md && node -e 'const de=require("./apps/web/src/messages/de.json"),en=require("./apps/web/src/messages/en.json");const w=(o,p,r)=>{for(const[k,v]of Object.entries(o||{})){const q=p+"."+k;if(v&&typeof v==="object")w(v,q,r);else r[q]=v}return r};const a=w(de.domains,"domains",{}),b=w(en.domains,"domains",{});if(Object.keys(a).sort().join()!==Object.keys(b).sort().join()||Object.keys(a).some(k=>k.startsWith("domains.settings.nameServers"))){console.error("keys");process.exit(1)}' && docker compose ps --status running --services | grep -qx api && docker compose ps --status running --services | grep -qx web && test "$(docker compose exec -T db psql -U tessera -d tessera -tAc "SELECT count(*) FROM information_schema.columns WHERE table_name='DomainsConfig' AND column_name='defaultNameServers'")" = "0" && A=$(mktemp) && curl -sf -c "$A" -H 'Content-Type: application/json' -d '{"username":"admin","password":"admin123"}' http://localhost:3001/auth/login >/dev/null && MID=$(curl -sf -b "$A" http://localhost:3001/modules/catalog | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const m=JSON.parse(s).find(x=>x.slug==="domains");if(!m)process.exit(1);process.stdout.write(m.isActiveForTenant?"":m.id)})') && { [ -z "$MID" ] || curl -sf -b "$A" -X POST "http://localhost:3001/modules/$MID/activate" >/dev/null; } && S=$(curl -sf -b "$A" http://localhost:3001/modules/domains/settings) && echo "$S" | grep -q '"hasPassword"' && ! echo "$S" | grep -q defaultNameServers && C=$(curl -s -o /dev/null -w '%{http_code}' -b "$A" http://localhost:3001/modules/domains/name-servers) && case "$C" in 200|409|502|504) echo "final gates ok (name-servers $C)";; *) echo "name-servers answered $C"; exit 1;; esac</automated>
|
||
<human-check>End of task, with Demo credentials entered by the user (record as checklist H-1..H-4 in the SUMMARY; not run by the executor): H-1 [ASSUMED] key names — Registrieren tab lists the Demo user's standard nameservers in the AutoDNS order; if instead the hint "In AutoDNS sind keine Standard-Nameserver hinterlegt" appears although AutoDNS has values, read the warning line "Keine Standard-Nameserver im AutoDNS-Profil erkannt" in `docker compose logs api` (it lists the key names) and adapt parseProfileNameServers. H-2 the AutoDNS user may read its own profile (no 403; a 403 shows the unreadable hint). H-3 a demo registration sends the shown nameservers and AutoDNS accepts them explicitly. H-4 Live: the list matches the four values the user named, in that order. H-x replaces item 8 (A8) of the 261008-dts demo checklist, whose step "Standard-Nameserver in den Einstellungen eintragen" no longer exists.</human-check>
|
||
</verify>
|
||
<done>No "Standard-Nameserver" card in Einstellungen, no nameserver field in settings/status API and web types, column dropped by migration 20261008160000 and absent in the local DB; docs and the existing CHANGELOG Domains lines describe nameservers from AutoDNS; full api and web suites, both tsc runs and biome on touched files are green; the rebuilt stack serves GET name-servers without 404/500.</done>
|
||
</task>
|
||
|
||
</tasks>
|
||
|
||
<threat_model>
|
||
## Trust Boundaries
|
||
|
||
| Boundary | Description |
|
||
|----------|-------------|
|
||
| browser -> API | Manager-controlled request bodies for draft creation and submit; the nameserver list must not be taken from here |
|
||
| API -> AutoDNS | Profile read (new GET) and the existing single POST /domain; AutoDNS answers are untrusted input to the parser |
|
||
| API -> logs | New warning line when no nameservers are recognised |
|
||
|
||
## STRIDE Threat Register
|
||
|
||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||
| T-h3t-01 | Tampering | createOrder request body | high | mitigate | Field removed from CreateDomainsOrderDto (whitelist strips it); server reads the profile itself; test proves a sent list is ignored (Task 1) |
|
||
| T-h3t-02 | Tampering | draft vs. confirmed order | high | mitigate | List stored in the draft payload; summary returns it; WR-02 `updatedAt` binding in the claim unchanged; re-creating a draft re-reads the profile and changes the version (Task 1 test) |
|
||
| T-h3t-03 | Tampering | profile path built from the stored AutoDNS user name | medium | mitigate | `encodeURIComponent` on the user name plus the client's existing path check; a thrown path error becomes 502 autodnsError, never a request to another path (Task 1) |
|
||
| T-h3t-04 | Repudiation / integrity | guessing nameservers | high | mitigate | Ambiguous or fewer than two recognised values -> 409 noDefaultNameServers, no draft; submit guard rejects drafts with fewer than two nameservers before the claim; web keeps the summary button disabled (Tasks 1, 2) |
|
||
| T-h3t-05 | Information Disclosure | warning log line | low | mitigate | Logs only profile key names (max 50, 60 chars each), never values or credentials (Task 1) |
|
||
| T-h3t-06 | Denial of Service | extra AutoDNS calls | low | accept | One profile GET per Registrieren load and per draft, Verwalten only, through the shared 350 ms limiter, no retry |
|
||
| T-h3t-07 | Elevation of Privilege | GET name-servers | medium | mitigate | `@ModuleManage('domains')`, no role decorator, class-level `@UseModule`; controller spec and module-manage-handlers spec assert it (Task 1) |
|
||
| T-h3t-08 | Tampering | money-safety path | critical | mitigate | Claim, single POST, outcome mapping, UNKNOWN recovery, reconcile and cancel untouched; all existing submit/claim/reconcile tests must pass unchanged (Task 1 verify, Task 3 full suite) |
|
||
| T-h3t-SC | Tampering | npm/pip/cargo installs | high | accept | No package installs in this plan; nothing to audit |
|
||
</threat_model>
|
||
|
||
<verification>
|
||
- Task 1: api domains specs, module-manage-handlers, rls-coverage, rls-access-inventory and api tsc green; tracer chain test present; no role decorator in the controller.
|
||
- Task 2: web domains tests and message guards green, web tsc green, de/en key parity, hint text starts with the requested sentence.
|
||
- Task 3: full api and web suites, both tsc runs, biome on touched files, no old field in apps/*/src, no company-specific nameserver hostnames in code, docs or changelog, migration present and applied (column absent in the local DB), rebuilt api/web running, GET name-servers answers 200/409/502/504.
|
||
</verification>
|
||
|
||
<success_criteria>
|
||
- The setting "Standard-Nameserver" is gone from UI, API, DTOs, types and database (D-01).
|
||
- Registration uses only the nameservers AutoDNS holds in the user profile, recognised by one tested function with the [ASSUMED] key rule (D-02).
|
||
- Registrieren and the summary show them read-only in AutoDNS order; the draft stores them, the one POST /domain sends them in that order, WR-02 still binds the confirmation (D-03).
|
||
- Without readable nameservers, a clear German hint appears and registration is locked in browser and server (D-04).
|
||
- Docs and the existing CHANGELOG Domains lines updated; 261008-dts SUMMARY untouched (D-05).
|
||
- Money safety unchanged; every pre-existing test still green.
|
||
</success_criteria>
|
||
|
||
<output>
|
||
Create `.planning/quick/261008-h3t-domains-nameserver-aus-autodns-statt-tes/261008-h3t-SUMMARY.md` when done. Include: commits per task, test/gate measurements, the demo checklist H-1..H-4 from the Task 3 human-check (H-1 marked [ASSUMED], with the log-line instructions), the note that it supersedes item 8 of the 261008-dts checklist, and browser steps for the orchestrator (dark mode): Einstellungen without a nameserver card; Registrieren with the read-only list or the hint plus "Erneut aus AutoDNS lesen" and a disabled "Zusammenfassung anzeigen".
|
||
</output>
|