From 2176f8ecce233a5e3c6416e866f73d2a8823a37e Mon Sep 17 00:00:00 2001 From: Schalli Date: Thu, 8 Oct 2026 12:37:21 +0200 Subject: [PATCH] refactor(domains): Einstellung Standard-Nameserver entfernt, Doku angepasst (h3t) - Spalte DomainsConfig.defaultNameServers per Migration 20261008160000 entfernt - Feld in Einstellungen/Status (API, DTO, Typen) und Karte im Web entfernt - Anleitungen und CHANGELOG: Nameserver kommen aus AutoDNS Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 4 +- .../migration.sql | 3 + apps/api/prisma/schema.prisma | 1 - .../domains/domains-settings.service.spec.ts | 35 +----- .../src/domains/domains-settings.service.ts | 44 +------ apps/api/src/domains/domains.types.ts | 2 - .../domains/dto/domains-settings.dto.spec.ts | 2 - .../src/domains/dto/domains-settings.dto.ts | 20 +--- .../domains/components/RegisterTab.test.tsx | 1 - .../domains/components/SettingsTab.tsx | 108 ------------------ .../modules/domains/domains-page.test.tsx | 18 +-- apps/web/src/lib/domains-api.ts | 3 - apps/web/src/messages/de.json | 7 -- apps/web/src/messages/en.json | 7 -- docs/anleitung-administration.md | 4 +- docs/anleitung-anwender.md | 2 +- ...andantentrennung-zugriffsklassifikation.md | 2 +- 17 files changed, 22 insertions(+), 241 deletions(-) create mode 100644 apps/api/prisma/migrations/20261008160000_domains_drop_default_nameservers/migration.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index e239a60..3357dbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,10 @@ Diese Liste beschreibt in einfachen Worten, was sich von Version zu Version an T ### Neu - Neues Modul „Domains“ in der Gruppe „Domains“: Domains bei AutoDNS registrieren, Kontakte und Kunden zuordnen. Ein Administrator aktiviert das Modul im Marktplatz; wer es nutzen soll, bekommt zusätzlich die Freigabe. Mit „Benutzen“ sehen Sie die Domainliste, die Kontakte, die Kunden und die Aufträge; mit „Verwalten“ (und als Administrator) registrieren Sie Domains, legen Kontakte und Kunden an und richten die Anbindung ein. -- Domains, Anbindung an AutoDNS: Unter „Einstellungen“ tragen Sie Benutzername, Passwort und Kontext getrennt für das Demo-System (zum Ausprobieren) und das Live-System (echte, kostenpflichtige Registrierungen) ein. Das Passwort wird verschlüsselt gespeichert und nie wieder angezeigt. Neue Installationen starten im Demo-System; der Wechsel auf das Live-System verlangt eine ausdrückliche Bestätigung, und eine Kennzeichnung oben auf der Seite zeigt jederzeit, welches System gerade gilt. „Verbindung testen“ prüft den Zugang mit einem einzigen Aufruf. Dazu kommen die Standard-Nameserver, die bei jeder Registrierung vorausgefüllt werden. +- Domains, Anbindung an AutoDNS: Unter „Einstellungen“ tragen Sie Benutzername, Passwort und Kontext getrennt für das Demo-System (zum Ausprobieren) und das Live-System (echte, kostenpflichtige Registrierungen) ein. Das Passwort wird verschlüsselt gespeichert und nie wieder angezeigt. Neue Installationen starten im Demo-System; der Wechsel auf das Live-System verlangt eine ausdrückliche Bestätigung, und eine Kennzeichnung oben auf der Seite zeigt jederzeit, welches System gerade gilt. „Verbindung testen“ prüft den Zugang mit einem einzigen Aufruf. - Domains, Kontakte und Kunden: Alle Kontakte, die bei AutoDNS vorhanden sind, erscheinen automatisch in Tessera („Aus AutoDNS neu einlesen“ holt sie auf Wunsch sofort neu; das dürfen Benutzer mit der Freigabe „Verwalten“). Neue Kontakte (Person oder Organisation) legen Sie über ein Formular an. Jeden Kontakt – einzeln oder viele auf einmal – ordnen Sie einem Kunden zu; Ihre eigene Firma legen Sie ebenfalls als Kunden an und markieren sie. Die Listen lassen sich nach Kunde filtern und gruppieren. - Domains, Domainliste: Alle Domains aus AutoDNS mit Kunde (über den Inhaber-Kontakt), Inhaber, Ablaufdatum und Status, ebenfalls filterbar und nach Kunde gruppierbar. -- Domains, Registrieren: Verfügbarkeit prüfen, Kontakte und Nameserver wählen, Zusammenfassung mit System und Preis ansehen und mit „Jetzt verbindlich registrieren“ bestätigen. Tessera schützt vor Doppelbestellungen: Jede Registrierung wird höchstens ein einziges Mal an AutoDNS geschickt, auch bei Doppelklick oder zwei geöffneten Fenstern, und nie automatisch wiederholt. Ist unklar, ob AutoDNS die Bestellung angenommen hat, steht der Auftrag auf „Ergebnis ungeklärt“, bis Tessera ihn mit AutoDNS abgeglichen hat. Den Stand jeder Registrierung sehen Sie im Reiter „Aufträge“; er wird beim Öffnen und danach alle 30 Sekunden mit AutoDNS abgeglichen. +- Domains, Registrieren: Verfügbarkeit prüfen, 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), Zusammenfassung mit System und Preis ansehen und mit „Jetzt verbindlich registrieren“ bestätigen. Tessera schützt vor Doppelbestellungen: Jede Registrierung wird höchstens ein einziges Mal an AutoDNS geschickt, auch bei Doppelklick oder zwei geöffneten Fenstern, und nie automatisch wiederholt. Ist unklar, ob AutoDNS die Bestellung angenommen hat, steht der Auftrag auf „Ergebnis ungeklärt“, bis Tessera ihn mit AutoDNS abgeglichen hat. Den Stand jeder Registrierung sehen Sie im Reiter „Aufträge“; er wird beim Öffnen und danach alle 30 Sekunden mit AutoDNS abgeglichen. ### Geändert diff --git a/apps/api/prisma/migrations/20261008160000_domains_drop_default_nameservers/migration.sql b/apps/api/prisma/migrations/20261008160000_domains_drop_default_nameservers/migration.sql new file mode 100644 index 0000000..627bb8f --- /dev/null +++ b/apps/api/prisma/migrations/20261008160000_domains_drop_default_nameservers/migration.sql @@ -0,0 +1,3 @@ +-- quick-261008-h3t: Die Standard-Nameserver kommen aus dem AutoDNS-Benutzerprofil. +-- Die Spalte wird nicht mehr gelesen, ihre Werte sind wertlos. +ALTER TABLE "DomainsConfig" DROP COLUMN "defaultNameServers"; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 4094a71..c1846ba 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -876,7 +876,6 @@ model DomainsConfig { liveUser String? liveEncryptedPassword String? liveContext Int? - defaultNameServers String[] @default([]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/apps/api/src/domains/domains-settings.service.spec.ts b/apps/api/src/domains/domains-settings.service.spec.ts index a5f9c48..f2cbbed 100644 --- a/apps/api/src/domains/domains-settings.service.spec.ts +++ b/apps/api/src/domains/domains-settings.service.spec.ts @@ -20,7 +20,6 @@ interface Row { liveUser: string | null; liveEncryptedPassword: string | null; liveContext: number | null; - defaultNameServers: string[]; updatedAt: Date; } @@ -33,7 +32,6 @@ function emptyRow(over: Partial = {}): Row { liveUser: null, liveEncryptedPassword: null, liveContext: null, - defaultNameServers: [], updatedAt: new Date('2026-10-08T10:00:00Z'), ...over, }; @@ -79,7 +77,6 @@ describe('DomainsSettingsService — Lesen', () => { environment: 'DEMO', demo: { user: null, hasPassword: false, context: null }, live: { user: null, hasPassword: false, context: null }, - defaultNameServers: [], configured: { demo: false, live: false }, }); }); @@ -91,10 +88,15 @@ describe('DomainsSettingsService — Lesen', () => { configured: true, demoConfigured: true, liveConfigured: false, - defaultNameServers: [], }); }); + it('Einstellungen und Status tragen keine Nameserver (h3t: sie kommen aus AutoDNS)', async () => { + const { service } = makeService(configuredDemo()); + expect(Object.keys(await service.getSettings('t1')).join()).not.toMatch(/nameserver/i); + expect(Object.keys(await service.getStatus('t1')).join()).not.toMatch(/nameserver/i); + }); + it('aktives LIVE ohne Live-Zugang gilt als nicht eingerichtet', async () => { const { service } = makeService({ ...configuredDemo(), environment: 'LIVE' }); const status = await service.getStatus('t1'); @@ -156,31 +158,6 @@ describe('DomainsSettingsService — Speichern', () => { await expect(service.saveSettings('t1', { environment: 'LIVE' })).resolves.toBeDefined(); await expect(service.saveSettings('t1', { environment: 'DEMO' })).resolves.toBeDefined(); }); - - it('normalisiert Nameserver und prueft Anzahl und Form', async () => { - const { service, upsert } = makeService(null); - await service.saveSettings('t1', { - defaultNameServers: [' NS1.Example.COM ', 'ns2.example.com'], - }); - expect(upsert.mock.calls[0][0].update.defaultNameServers).toEqual([ - 'ns1.example.com', - 'ns2.example.com', - ]); - await expect( - service.saveSettings('t1', { defaultNameServers: ['ns1.example.com'] }), - ).rejects.toMatchObject({ response: { code: 'tooFewNameServers' } }); - await expect( - service.saveSettings('t1', { - defaultNameServers: Array.from({ length: 7 }, (_, i) => `ns${i}.example.com`), - }), - ).rejects.toBeInstanceOf(BadRequestException); - await expect( - service.saveSettings('t1', { defaultNameServers: ['ns1.example.com', 'kein rechner!'] }), - ).rejects.toMatchObject({ response: { code: 'invalidNameServer' } }); - // Leeren ist erlaubt - await service.saveSettings('t1', { defaultNameServers: [] }); - expect(upsert.mock.calls.at(-1)?.[0].update.defaultNameServers).toEqual([]); - }); }); describe('DomainsSettingsService — Zugang', () => { diff --git a/apps/api/src/domains/domains-settings.service.ts b/apps/api/src/domains/domains-settings.service.ts index 382ab26..ab51789 100644 --- a/apps/api/src/domains/domains-settings.service.ts +++ b/apps/api/src/domains/domains-settings.service.ts @@ -15,7 +15,6 @@ import { type AutodnsRateLimiter, autodnsRequest, } from './autodns-client'; -import { HOSTNAME_PATTERN } from './domain-name'; import { AUTODNS_AUTH_MESSAGE, type DomainsConnectionTestResult, @@ -38,9 +37,6 @@ const DECRYPT_FAILED = { 'Das gespeicherte AutoDNS-Passwort ließ sich nicht entschlüsseln. Bitte tragen Sie es in den Einstellungen neu ein.', }; -const MIN_NAMESERVERS = 2; -const MAX_NAMESERVERS = 6; - interface ConfigRow { environment: DomainsEnvironment; demoUser: string | null; @@ -49,7 +45,6 @@ interface ConfigRow { liveUser: string | null; liveEncryptedPassword: string | null; liveContext: number | null; - defaultNameServers: string[]; updatedAt: Date; } @@ -67,7 +62,8 @@ function isConfigured(user: string | null, password: string | null, context: num /** * Einstellungen des Moduls Domains (quick-261008-dts): getrennte, verschluesselt * abgelegte Zugaenge fuer das Demo- und das Live-System, Umschalter, - * Standard-Nameserver und der Verbindungstest. Das Passwort verlaesst diesen + * der Verbindungstest. Standard-Nameserver gibt es hier nicht: die liest + * `DomainsOrdersService` bei jeder Registrierung aus dem AutoDNS-Benutzerprofil. Das Passwort verlaesst diesen * Dienst nie in Richtung Client; Antworten tragen nur `hasPassword`. * Gesamter Zugriff auf `domainsConfig` liegt ausschliesslich hier. */ @@ -124,7 +120,6 @@ export class DomainsSettingsService { environment: row?.environment ?? 'DEMO', demo: this.envView(row, 'DEMO'), live: this.envView(row, 'LIVE'), - defaultNameServers: row?.defaultNameServers ?? [], configured: { demo: demoConfigured, live: liveConfigured }, }; } @@ -140,38 +135,11 @@ export class DomainsSettingsService { configured: view.environment === 'DEMO' ? view.configured.demo : view.configured.live, demoConfigured: view.configured.demo, liveConfigured: view.configured.live, - defaultNameServers: view.defaultNameServers, }; } // --- Speichern ------------------------------------------------------------- - private normalizeNameServers(raw: string[]): string[] { - const list = raw.map((n) => n.trim().toLowerCase()).filter((n) => n.length > 0); - if (list.length === 0) return []; - if (list.length > MAX_NAMESERVERS) { - throw new BadRequestException({ - code: 'tooManyNameServers', - message: 'Höchstens sechs Nameserver sind möglich.', - }); - } - for (const name of list) { - if (!HOSTNAME_PATTERN.test(name)) { - throw new BadRequestException({ - code: 'invalidNameServer', - message: `Der Nameserver „${name}“ ist kein gültiger Rechnername.`, - }); - } - } - if (list.length < MIN_NAMESERVERS) { - throw new BadRequestException({ - code: 'tooFewNameServers', - message: 'Bitte geben Sie mindestens zwei Nameserver an.', - }); - } - return list; - } - async saveSettings(tenantId: string, dto: SaveDomainsSettingsDto): Promise { const current = await this.loadRow(tenantId); @@ -196,9 +164,6 @@ export class DomainsSettingsService { // Leeres oder fehlendes Passwort = gespeichertes bleibt (Muster LDAP). if (dto.demoPassword) data.demoEncryptedPassword = this.crypto.encrypt(dto.demoPassword); if (dto.livePassword) data.liveEncryptedPassword = this.crypto.encrypt(dto.livePassword); - if (dto.defaultNameServers !== undefined) { - data.defaultNameServers = this.normalizeNameServers(dto.defaultNameServers); - } const tenantPrisma = forTenant(this.prisma, tenantId); const saved = await tenantPrisma.domainsConfig.upsert({ @@ -246,11 +211,6 @@ export class DomainsSettingsService { return { environment, credentials, configVersion: row.updatedAt.getTime() }; } - /** Standard-Nameserver fuer das Registrierungsformular. */ - async getDefaultNameServers(tenantId: string): Promise { - return (await this.loadRow(tenantId))?.defaultNameServers ?? []; - } - // --- Verbindungstest ------------------------------------------------------- /** diff --git a/apps/api/src/domains/domains.types.ts b/apps/api/src/domains/domains.types.ts index 06128de..afa9a31 100644 --- a/apps/api/src/domains/domains.types.ts +++ b/apps/api/src/domains/domains.types.ts @@ -16,7 +16,6 @@ export interface DomainsSettingsView { environment: DomainsEnvironment; demo: DomainsEnvironmentView; live: DomainsEnvironmentView; - defaultNameServers: string[]; configured: { demo: boolean; live: boolean }; } @@ -26,7 +25,6 @@ export interface DomainsStatusView { configured: boolean; demoConfigured: boolean; liveConfigured: boolean; - defaultNameServers: string[]; } export interface DomainsConnectionTestResult { diff --git a/apps/api/src/domains/dto/domains-settings.dto.spec.ts b/apps/api/src/domains/dto/domains-settings.dto.spec.ts index 6e2fa42..75ff334 100644 --- a/apps/api/src/domains/dto/domains-settings.dto.spec.ts +++ b/apps/api/src/domains/dto/domains-settings.dto.spec.ts @@ -17,7 +17,6 @@ describe('SaveDomainsSettingsDto — null ist keine fehlende Angabe', () => { environment: 'DEMO', demoUser: 'u', demoPassword: '', - defaultNameServers: ['ns1.example.com', 'ns2.example.com'], }), ).toEqual([]); }); @@ -29,7 +28,6 @@ describe('SaveDomainsSettingsDto — null ist keine fehlende Angabe', () => { 'demoPassword', 'liveUser', 'livePassword', - 'defaultNameServers', ])('%s: null -> Validierungsfehler (400) statt TypeError im Dienst', async (field) => { expect(await errorsOf({ [field]: null })).toEqual([field]); }); diff --git a/apps/api/src/domains/dto/domains-settings.dto.ts b/apps/api/src/domains/dto/domains-settings.dto.ts index 3dbf7c7..351ab86 100644 --- a/apps/api/src/domains/dto/domains-settings.dto.ts +++ b/apps/api/src/domains/dto/domains-settings.dto.ts @@ -1,15 +1,4 @@ -import { - ArrayMaxSize, - IsArray, - IsBoolean, - IsIn, - IsInt, - IsString, - Max, - MaxLength, - Min, - ValidateIf, -} from 'class-validator'; +import { IsBoolean, IsIn, IsInt, IsString, Max, MaxLength, Min, ValidateIf } from 'class-validator'; /** * Teilspeichern: ein Feld darf FEHLEN (`undefined`), aber nicht `null` sein — @@ -69,13 +58,6 @@ export class SaveDomainsSettingsDto { @Min(1) @Max(2147483647) liveContext?: number | null; - - @ValidateIf(isPresent) - @IsArray() - @ArrayMaxSize(6) - @IsString({ each: true }) - @MaxLength(253, { each: true }) - defaultNameServers?: string[]; } export class TestDomainsConnectionDto { diff --git a/apps/web/src/app/(portal)/modules/domains/components/RegisterTab.test.tsx b/apps/web/src/app/(portal)/modules/domains/components/RegisterTab.test.tsx index f6ad0be..7c352d2 100644 --- a/apps/web/src/app/(portal)/modules/domains/components/RegisterTab.test.tsx +++ b/apps/web/src/app/(portal)/modules/domains/components/RegisterTab.test.tsx @@ -48,7 +48,6 @@ function status(over: Partial = {}): DomainsStatus { configured: true, demoConfigured: true, liveConfigured: true, - defaultNameServers: [], ...over, }; } diff --git a/apps/web/src/app/(portal)/modules/domains/components/SettingsTab.tsx b/apps/web/src/app/(portal)/modules/domains/components/SettingsTab.tsx index 30e2921..4e9e0d2 100644 --- a/apps/web/src/app/(portal)/modules/domains/components/SettingsTab.tsx +++ b/apps/web/src/app/(portal)/modules/domains/components/SettingsTab.tsx @@ -24,8 +24,6 @@ const SECONDARY_BUTTON = /** Der Live-Kontext von AutoDNS ist in der Regel 4; das Demo-System hat keinen Vorschlag. */ const LIVE_DEFAULT_CONTEXT = 4; -const MIN_NAMESERVER_ROWS = 2; -const MAX_NAMESERVER_ROWS = 6; type Message = { kind: 'ok' | 'error'; text: string }; @@ -349,111 +347,6 @@ function AccessCard({ ); } -// --- Standard-Nameserver -------------------------------------------------------- - -function padNameServers(list: string[]): string[] { - const rows = [...list]; - while (rows.length < MIN_NAMESERVER_ROWS) rows.push(''); - return rows; -} - -function NameServersCard({ - settings, - onSaved, -}: { - settings: DomainsSettings; - onSaved: (s: DomainsSettings) => void; -}) { - const t = useTranslations('domains.settings'); - const errorText = useErrorText(); - const [rows, setRows] = useState(padNameServers(settings.defaultNameServers)); - const [saving, setSaving] = useState(false); - const [message, setMessage] = useState(null); - - useEffect(() => { - setRows(padNameServers(settings.defaultNameServers)); - }, [settings.defaultNameServers]); - - const cleaned = rows.map((r) => r.trim().toLowerCase()).filter((r) => r.length > 0); - const dirty = cleaned.join('\n') !== settings.defaultNameServers.join('\n'); - - const save = async () => { - setSaving(true); - setMessage(null); - try { - onSaved(await saveDomainsSettings({ defaultNameServers: cleaned })); - setMessage({ kind: 'ok', text: t('saved') }); - } catch (error) { - setMessage({ kind: 'error', text: errorText(error) }); - } finally { - setSaving(false); - } - }; - - return ( - -
- -
- - - } - > -
- {rows.map((value, index) => ( - // biome-ignore lint/suspicious/noArrayIndexKey: Zeilen sind positionsgebunden und haben keine Kennung -
-
- - setRows(rows.map((r, i) => (i === index ? e.target.value : r)))} - className={INPUT_CLASS} - /> -
- {rows.length > MIN_NAMESERVER_ROWS && ( - - )} -
- ))} - {rows.length < MAX_NAMESERVER_ROWS && ( - - )} -
-
- ); -} - // --- Reiter --------------------------------------------------------------------- /** @@ -503,7 +396,6 @@ export function SettingsTab({ onChanged }: { onChanged: () => void }) { - ); } diff --git a/apps/web/src/app/(portal)/modules/domains/domains-page.test.tsx b/apps/web/src/app/(portal)/modules/domains/domains-page.test.tsx index c331669..ee857a3 100644 --- a/apps/web/src/app/(portal)/modules/domains/domains-page.test.tsx +++ b/apps/web/src/app/(portal)/modules/domains/domains-page.test.tsx @@ -70,7 +70,6 @@ function status(over: Partial = {}): DomainsStatus { configured: true, demoConfigured: true, liveConfigured: false, - defaultNameServers: [], ...over, }; } @@ -80,7 +79,6 @@ function settings(over: Partial = {}): DomainsSettings { environment: 'DEMO', demo: { user: 'api-user', hasPassword: true, context: 1 }, live: { user: null, hasPassword: false, context: null }, - defaultNameServers: [], configured: { demo: true, live: false }, ...over, }; @@ -354,19 +352,11 @@ describe('Einstellungen', () => { expect(within(demoCard).getByText('Bitte zuerst speichern')).toBeTruthy(); }); - it('Nameserver: speichert die bereinigte Liste', async () => { + it('Einstellungen zeigen keine Karte "Standard-Nameserver" mehr (h3t)', async () => { await renderSettings(); - const first = (await screen.findByLabelText('Nameserver 1')) as HTMLInputElement; - fireEvent.change(first, { target: { value: ' NS1.Example.com ' } }); - fireEvent.change(screen.getByLabelText('Nameserver 2'), { - target: { value: 'ns2.example.com' }, - }); - const card = document.getElementById('domains-nameservers') as HTMLElement; - fireEvent.click(within(card).getByRole('button', { name: 'Speichern' })); - await waitFor(() => expect(mockSaveSettings).toHaveBeenCalledTimes(1)); - expect(mockSaveSettings).toHaveBeenCalledWith({ - defaultNameServers: ['ns1.example.com', 'ns2.example.com'], - }); + await screen.findByRole('radio', { name: /Live-System \(kostenpflichtig\)/ }); + expect(screen.queryByText('Standard-Nameserver')).toBeNull(); + expect(document.getElementById('domains-nameservers')).toBeNull(); }); }); diff --git a/apps/web/src/lib/domains-api.ts b/apps/web/src/lib/domains-api.ts index 43a1928..d556fb8 100644 --- a/apps/web/src/lib/domains-api.ts +++ b/apps/web/src/lib/domains-api.ts @@ -16,7 +16,6 @@ export interface DomainsStatus { configured: boolean; demoConfigured: boolean; liveConfigured: boolean; - defaultNameServers: string[]; } export interface DomainsEnvironmentAccess { @@ -29,7 +28,6 @@ export interface DomainsSettings { environment: DomainsEnvironment; demo: DomainsEnvironmentAccess; live: DomainsEnvironmentAccess; - defaultNameServers: string[]; configured: { demo: boolean; live: boolean }; } @@ -43,7 +41,6 @@ export interface SaveDomainsSettingsInput { liveUser?: string; livePassword?: string; liveContext?: number | null; - defaultNameServers?: string[]; } export interface ConnectionTestResult { diff --git a/apps/web/src/messages/de.json b/apps/web/src/messages/de.json index 942fd49..05ad966 100644 --- a/apps/web/src/messages/de.json +++ b/apps/web/src/messages/de.json @@ -2100,13 +2100,6 @@ "test": "Verbindung testen", "testing": "Verbindung wird getestet …", "saveFirst": "Bitte zuerst speichern" - }, - "nameServers": { - "title": "Standard-Nameserver", - "description": "Diese Nameserver werden bei jeder Registrierung vorausgefüllt. Sie müssen bei Ihrem Anbieter bereits eingerichtet sein, sonst scheitert die Registrierung.", - "label": "Nameserver {number}", - "add": "Nameserver hinzufügen", - "remove": "Entfernen" } }, "common": { diff --git a/apps/web/src/messages/en.json b/apps/web/src/messages/en.json index 8ddac1c..4c563a6 100644 --- a/apps/web/src/messages/en.json +++ b/apps/web/src/messages/en.json @@ -2100,13 +2100,6 @@ "test": "Test connection", "testing": "Testing connection …", "saveFirst": "Please save first" - }, - "nameServers": { - "title": "Default name servers", - "description": "These name servers are prefilled for every registration. They must already be set up with your provider, otherwise the registration fails.", - "label": "Name server {number}", - "add": "Add name server", - "remove": "Remove" } }, "common": { diff --git a/docs/anleitung-administration.md b/docs/anleitung-administration.md index 26f5086..5eac195 100644 --- a/docs/anleitung-administration.md +++ b/docs/anleitung-administration.md @@ -360,11 +360,11 @@ Das Modul „Nextcloud-Status“ (Gruppe Infrastruktur) aktivieren Sie wie jedes Das Modul „Domains“ (Gruppe Domains) aktivieren Sie wie jedes Modul im Marktplatz; wer es nutzen soll, bekommt die Freigabe „Benutzen“. **Die Anbindung einrichten, Kunden und Kontakte anlegen und Domains registrieren** dürfen Administratoren und Benutzer mit der Freigabestufe „Verwalten“ – vergeben Sie diese Stufe nur an Personen, denen Sie Bestellungen auf Ihre Kosten zutrauen, denn eine Registrierung im Live-System ist verbindlich und kostenpflichtig. -- **Eigener AutoDNS-Benutzer:** Legen Sie bei AutoDNS einen eigenen Benutzer für Tessera an, **ohne Zwei-Faktor-Anmeldung** – Tessera meldet sich mit Benutzername, Passwort und der Kontextnummer an, einen Zusatzcode kann es nicht eingeben. Der Benutzer braucht die Rechte, Domains und Kontakte zu lesen sowie Kontakte anzulegen und Domains zu registrieren. +- **Eigener AutoDNS-Benutzer:** Legen Sie bei AutoDNS einen eigenen Benutzer für Tessera an, **ohne Zwei-Faktor-Anmeldung** – Tessera meldet sich mit Benutzername, Passwort und der Kontextnummer an, einen Zusatzcode kann es nicht eingeben. Der Benutzer braucht die Rechte, Domains und Kontakte zu lesen sowie Kontakte anzulegen und Domains zu registrieren; außerdem muss er sein eigenes Benutzerprofil lesen dürfen (daraus liest Tessera die Standard-Nameserver). - **Kontext je System:** Tragen Sie unter Domains → Einstellungen für das Demo-System und das Live-System je Benutzername, Passwort und Kontext ein. Beim Live-System ist der Kontext in der Regel **4** (das Feld ist damit vorbelegt). Für das Demo-System nennt InterNetX den Wert; er steht nicht fest und ist deshalb frei eintragbar. Das Passwort wird verschlüsselt gespeichert und nie wieder angezeigt; lassen Sie das Feld später leer, um es zu behalten. - **Mit dem Demo-System beginnen:** Neue Installationen laufen im Demo-System, dort entstehen keine echten Registrierungen. Probieren Sie zuerst dort alles aus. Der Wechsel auf das Live-System (Einstellungen → System) verlangt eine ausdrückliche Bestätigung; danach zeigt die Kennzeichnung oben auf der Seite „Live-System – Registrierungen kosten Geld“. Für jedes System gelten eigene Zugangsdaten, eigene Kontakt-Zuordnungen und eigene Aufträge – ein Entwurf aus dem Demo-System kann nie im Live-System bestellt werden. - **Verbindung testen:** Der Knopf schickt genau einen Anmeldeversuch an AutoDNS. Klicken Sie bei einer Fehlermeldung nicht wiederholt: **Mehrere falsche Anmeldungen hintereinander können den AutoDNS-Benutzer sperren.** Prüfen Sie erst Benutzername, Passwort und Kontext, speichern Sie, und testen Sie dann einmal. -- **Standard-Nameserver:** Unter „Standard-Nameserver“ (zwei bis sechs) hinterlegen Sie die Nameserver, die bei jeder Registrierung vorausgefüllt werden. Sie **müssen bei Ihrem Anbieter bereits eingerichtet sein**: Für `.de`-Domains prüft die Registry (DENIC) die Nameserver bei der Registrierung, und eine Registrierung mit nicht eingerichteten Nameservern scheitert („Fehlgeschlagen“). +- **Nameserver kommen aus AutoDNS:** Tessera hat keine eigene Nameserver-Einstellung. Bei jeder Registrierung liest es die Standard-Nameserver aus dem AutoDNS-Benutzerprofil des in den Einstellungen eingetragenen AutoDNS-Benutzers (auch von einem übergeordneten Benutzer geerbte Werte) und verwendet sie unverändert und in der dort hinterlegten Reihenfolge. Hinterlegen Sie dort mindestens zwei Nameserver. Sie **müssen eingerichtet sein**: Für `.de`-Domains prüft die Registry (DENIC) die Nameserver bei der Registrierung, und eine Registrierung mit nicht eingerichteten Nameservern scheitert („Fehlgeschlagen“). Findet Tessera keine Nameserver oder kann es sie nicht lesen, zeigt der Reiter „Registrieren“ einen Hinweis, und die Registrierung ist nicht möglich. - **Netzwerk:** Der API-Container braucht ausgehenden Zugriff (https) auf `api.autodns.com` (Live) und `api.demo.autodns.com` (Demo). Die Adressen sind fest eingebaut und nicht änderbar; Zertifikate werden geprüft, Weiterleitungen nicht befolgt. - **Abfragegrenze:** AutoDNS erlaubt etwa drei Anfragen pro Sekunde und IP-Adresse. Tessera hält das ein (höchstens eine Anfrage alle 350 Millisekunden, Listen seitenweise, ein Zwischenspeicher von einer Minute) und wiederholt nie selbstständig eine Anfrage. Listen zeigen höchstens die ersten 2000 Einträge. - **Keine Doppelbestellung:** Jede Registrierung wird höchstens einmal an AutoDNS geschickt. Bei Zeitüberschreitung oder Verbindungsabbruch steht der Auftrag auf „Ergebnis ungeklärt“ und wird beim nächsten Abgleich (Reiter „Aufträge“) mit AutoDNS verglichen. Ein unklarer Auftrag lässt sich erst verwerfen, nachdem dieser Abgleich ohne Fund durchgelaufen ist. diff --git a/docs/anleitung-anwender.md b/docs/anleitung-anwender.md index e4bd88e..46267f5 100644 --- a/docs/anleitung-anwender.md +++ b/docs/anleitung-anwender.md @@ -174,7 +174,7 @@ Mit dem Modul „Domains“ (Gruppe **Domains**) verwalten Sie Ihre Domains bei **Eine Domain registrieren** (Reiter „Registrieren“, nur mit „Verwalten“): 1. Domain eingeben und **„Verfügbarkeit prüfen“**. Nur „frei“ lässt sich registrieren; der Preis für ein Jahr wird angezeigt, sofern AutoDNS ihn nennt. -2. Für **Inhaber, Admin-Kontakt, technischen Kontakt und Zonenkontakt** je einen Kontakt wählen. Admin-, Technik- und Zonenkontakt sind vorausgewählt (Admin wie der Inhaber, Technik und Zone wie der erste Kontakt Ihrer eigenen Firma, sonst wie der Inhaber) und lassen sich ändern. Die **Nameserver** sind aus den Einstellungen vorausgefüllt; es sind zwei bis sechs nötig. +2. Für **Inhaber, Admin-Kontakt, technischen Kontakt und Zonenkontakt** je einen Kontakt wählen. Admin-, Technik- und Zonenkontakt sind vorausgewählt (Admin wie der Inhaber, Technik und Zone wie der erste Kontakt Ihrer eigenen Firma, sonst wie der Inhaber) und lassen sich ändern. Darunter sehen Sie – nur zur Kontrolle – die in AutoDNS als Standard hinterlegten **Nameserver**, in der dort hinterlegten Reihenfolge. Sie lassen sich nur in AutoDNS ändern. Sind dort keine hinterlegt, erscheint ein Hinweis, und die Registrierung ist gesperrt. 3. **„Zusammenfassung anzeigen“** und alles prüfen: System, Domain, Laufzeit (ein Jahr), Preis, Kontakte, Nameserver. 4. Das Häkchen zur Bestätigung setzen und **„Jetzt verbindlich registrieren“** klicken. Im Live-System kostet das Geld. diff --git a/docs/mandantentrennung-zugriffsklassifikation.md b/docs/mandantentrennung-zugriffsklassifikation.md index d8ad1f7..b108233 100644 --- a/docs/mandantentrennung-zugriffsklassifikation.md +++ b/docs/mandantentrennung-zugriffsklassifikation.md @@ -898,7 +898,7 @@ werden. | apps/api/src/domains/domains-directory.service.ts | domainsCustomer | muss-mandantengebunden | gebunden | **quick-261008-dts (Aufgabe 2):** neu — Kunden des Moduls Domains (Name je Organisation eindeutig, Markierung „eigene Firma“, höchstens eine). `tenantId`-Spalte vorhanden, Regel `tenant_isolation_policy` OHNE Benutzerdimension (Migration 20261008120000) — Organisationsdaten. Bewusst KEINE `system_read_policy`. Acht Rohtreffer über `const tenantPrisma = forTenant(this.prisma, tenantId)`, je Methode ein eigener Klient; jeder `where` trägt `tenantId` (auch `updateMany`/`deleteMany` über `id` UND `tenantId`, damit eine fremde Id 0 Treffer liefert und als 404 endet). Kein `include`/relationales `select`. | | apps/api/src/domains/domains-directory.service.ts | domainsContactAssignment | muss-mandantengebunden | gebunden | **quick-261008-dts (Aufgabe 2):** neu — Zuordnung AutoDNS-Kontakt → Kunde. Das System (Demo/Live) ist Teil des Schlüssels (`@@unique([tenantId, environment, autodnsContactId])`), weil Kontakt-Nummern beider Systeme zwangsläufig kollidieren; jede Abfrage filtert auf das AKTIVE System. `tenantId`-Spalte vorhanden, Regel `tenant_isolation_policy` OHNE Benutzerdimension (Migration 20261008120000). Bewusst KEINE `system_read_policy`. Fünf Rohtreffer über `tenantPrisma` (`findMany`, `count`, zweimal `upsert`, `deleteMany`); der Fremdschlüssel auf den Kunden steht auf `Restrict`, ein Kunde mit Zuordnungen wird nicht gelöscht. Kein `include`/relationales `select`. | | apps/api/src/domains/domains-orders.service.ts | domainsOrder | muss-mandantengebunden | gebunden | **quick-261008-dts (Aufgabe 3):** neu — Registrierungsaufträge des Moduls Domains (Entwurf, Anspruch, Ergebnis, Prüfspur mit Benutzer und Zeitpunkt der Bestätigung). `tenantId`-Spalte vorhanden, Regel `tenant_isolation_policy` OHNE Benutzerdimension (Migration 20261008120000) — Organisationsdaten. Bewusst KEINE `system_read_policy`: kein Hintergrunddienst, der Stand wird beim Öffnen des Reiters abgeglichen. Fünfzehn Rohtreffer über `tenantPrisma`, je Methode ein eigener Klient; jeder `where` trägt `tenantId`, fremde Nummern enden als 404. Der Anspruch ist ein `updateMany` mit `status: 'DRAFT'` und dem aktiven System in der Bedingung; erst bei Zähler 1 geht `POST /domain` hinaus (Geld, höchstens einmal). Die Eindeutigkeit offener Aufträge je Domain und System sichert die Spalte `openKey` (`@@unique([tenantId, environment, openKey])`). Kein `include`/relationales `select`. | -| apps/api/src/domains/domains-settings.service.ts | domainsConfig | muss-mandantengebunden | gebunden | **quick-261008-dts:** neu — Einstellungen des Moduls Domains (AutoDNS), eine Zeile je Organisation (Singleton): aktives System (Demo/Live), getrennte Zugänge je System mit AES-verschlüsseltem Passwort (CryptoService, nie an den Client zurückgegeben), Standard-Nameserver. `tenantId`-Spalte vorhanden, Regel `tenant_isolation_policy` OHNE Benutzerdimension (Migration 20261008120000) — Einstellungen der Organisation, nicht persönliche Daten eines Benutzers. Bewusst KEINE `system_read_policy`: kein Hintergrunddienst, der Auftragsstatus wird beim Öffnen der Seite abgefragt. Zwei Rohtreffer über `const tenantPrisma = forTenant(this.prisma, tenantId)` (`loadRow` `findUnique`, `saveSettings` `upsert`); `where` trägt `tenantId`. | +| apps/api/src/domains/domains-settings.service.ts | domainsConfig | muss-mandantengebunden | gebunden | **quick-261008-dts:** neu — Einstellungen des Moduls Domains (AutoDNS), eine Zeile je Organisation (Singleton): aktives System (Demo/Live), getrennte Zugänge je System mit AES-verschlüsseltem Passwort (CryptoService, nie an den Client zurückgegeben). Spalte für Standard-Nameserver mit quick-261008-h3t entfernt (Migration 20261008160000). `tenantId`-Spalte vorhanden, Regel `tenant_isolation_policy` OHNE Benutzerdimension (Migration 20261008120000) — Einstellungen der Organisation, nicht persönliche Daten eines Benutzers. Bewusst KEINE `system_read_policy`: kein Hintergrunddienst, der Auftragsstatus wird beim Öffnen der Seite abgefragt. Zwei Rohtreffer über `const tenantPrisma = forTenant(this.prisma, tenantId)` (`loadRow` `findUnique`, `saveSettings` `upsert`); `where` trägt `tenantId`. | | apps/api/src/nextcloud-status/nextcloud-alert.service.ts | nextcloudAlertSubscription | muss-mandantengebunden | gebunden | **quick-261002-kxc:** neu — die persönliche Glocke „Benachrichtigen“ je Benutzer und Cloud. `tenantId`- und `userId`-Spalte vorhanden, Regel `tenant_isolation_policy` MIT Benutzerdimension (Migration 20261002170000, Form aus `Reminder`), bewusst OHNE `system_read_policy` — die Tabelle wird nie im Systemkontext gelesen. Jeder Zugriff läuft an den Mandanten gebunden: `subscribe`/`unsubscribe`/`subscribedInstanceIds` über `forTenant(this.prisma, tenantId, userId)` (Benutzer aus dem Token, nie aus dem Body), der Versand (`notifySubscribers`) über `forTenant(this.prisma, tenantId)` mit `where: { tenantId, instanceId }`; `listRecentAlerts` (Meldung in Tessera) liest nur die Abonnements des Aufrufers (`where: { tenantId, userId }`, Benutzer aus dem Token). | | apps/api/src/nextcloud-status/nextcloud-alert.service.ts | nextcloudInstance | muss-mandantengebunden | gebunden | **quick-261002-kxc:** `subscribe` prüft per `findFirst` mit `where: { id, tenantId }`, dass die Cloud dem Mandanten gehört (sonst 404); `evaluateAfterCheck` beansprucht den Übergang per `updateMany` mit `where: { id, tenantId, alertState }` VOR dem Mailversand (nur `count === 1` meldet, mehrere API-Instanzen und Neustarts melden nie doppelt). Beides an den Mandanten gebunden, kein Systemkontext. | | apps/api/src/nextcloud-status/nextcloud-alert.service.ts | user | muss-mandantengebunden | gebunden | **quick-261002-kxc:** `notifySubscribers` liest die Empfänger der Abonnements (`user.findMany` mit `where: { tenantId, id: { in } }`, nur skalare Felder E-Mail, Rolle, `isActive`), gebunden an den Mandanten der Cloud. Konto-Aktivität und Modulzugriff werden beim Senden erneut geprüft (nicht erst beim Einschalten der Glocke). |