feat(domains): Modul Domains mit AutoDNS-Zugang, Verbindungstest und Einstellungen

- Migration mit vier Tabellen (Einstellungen, Kunden, Kontaktzuordnung, Bestellungen), Zeilenschutz je Organisation
- AutoDNS-Client mit festen Demo-/Live-Adressen, Takt-Begrenzer, 20 s Zeitlimit, ohne Wiederholung
- Zugang je System verschluesselt gespeichert, Live-Wechsel nur mit Bestaetigung, Verbindungstest
- Modulseite mit Systemkennzeichnung und Reiter Einstellungen (nur Verwalten)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-08 10:35:21 +02:00
parent 6b0f840c1c
commit 1f1c984184
29 changed files with 3123 additions and 2 deletions
@@ -0,0 +1,165 @@
-- 261008-dts — Modul "Domains" (AutoDNS / InterNetX Domainrobot).
--
-- Zweck: vier neue Tabellen und zwei Aufzaehlungen.
-- * "DomainsConfig": Einstellungen der Organisation (Singleton), getrennte
-- Zugaenge fuer das Demo- und das Live-System, Passwoerter AES-verschluesselt
-- (CryptoService), Standard-Nameserver.
-- * "DomainsCustomer": Kunden, denen AutoDNS-Kontakte zugeordnet werden;
-- eine Zeile darf als "Eigene Firma" markiert sein (Pruefung im Dienst).
-- * "DomainsContactAssignment": Zuordnung AutoDNS-Kontakt -> Kunde. Die
-- Umgebung (DEMO/LIVE) gehoert zum Schluessel, weil Kontakt-Ids beider
-- Systeme kollidieren. Kontakte und Domains selbst werden NICHT gespiegelt,
-- AutoDNS bleibt die Quelle der Wahrheit.
-- * "DomainsOrder": Domain-Bestellungen (Geldsicherheit). "openKey" traegt den
-- Domainnamen, solange die Bestellung offen ist (DRAFT, SUBMITTING,
-- SUBMITTED, UNKNOWN, SUCCESS) und ist bei FAILED/CANCELED NULL. Die
-- Unique-Regel (tenantId, environment, openKey) verhindert zwei offene
-- Bestellungen derselben Domain; NULLs wiederholen sich in Postgres
-- beliebig. SUCCESS behaelt den Schluessel mit Absicht: kurz nach der
-- Registrierung kann ein verzoegerter WHOIS noch "frei" melden.
--
-- AutoDNS-Kontakt- und Job-Ids stehen als TEXT (opake Dezimalzahlen).
--
-- Von Hand geschrieben (Vorbild 20261002130000_handelsware_datev); der
-- DDL-Teil stammt aus `prisma migrate diff`, damit der Stand ohne Abweichung
-- zum Schema passt.
--
-- Zeilenschutz (Pflicht — sonst schlaegt rls-coverage.spec.ts fehl): alle
-- Tabellen tragen `tenantId` und `tenant_isolation_policy` OHNE
-- Benutzerdimension (`USING ("tenantId" = current_tenant_id())`) — das sind
-- Verwaltungsdaten der Organisation, keine persoenlichen Daten eines
-- Benutzers. Keine `system_read_policy`: es gibt keinen Hintergrunddienst, der
-- diese Tabellen ueber alle Organisationen liest (Auftragsstatus wird beim
-- Oeffnen der Seite abgefragt, nicht per Zeitplan).
--
-- Rechte fuer die Anwendungsrolle tessera_app kommen automatisch ueber
-- ALTER DEFAULT PRIVILEGES aus 20260909130000_rls_app_role — hier nichts zu
-- tun.
--
-- WICHTIG: wie alle bisherigen RLS-Migrationen wirken diese Regeln erst,
-- wenn die Anwendung als Rolle ohne Umgehungsrecht verbindet (Schalter
-- heute AUS, siehe docs/mandantentrennung-datenbankrolle.md).
-- CreateEnum
CREATE TYPE "AutodnsEnvironment" AS ENUM ('DEMO', 'LIVE');
-- CreateEnum
CREATE TYPE "DomainOrderStatus" AS ENUM ('DRAFT', 'SUBMITTING', 'SUBMITTED', 'SUCCESS', 'FAILED', 'UNKNOWN', 'CANCELED');
-- CreateTable
CREATE TABLE "DomainsConfig" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"environment" "AutodnsEnvironment" NOT NULL DEFAULT 'DEMO',
"demoUser" TEXT,
"demoEncryptedPassword" TEXT,
"demoContext" INTEGER,
"liveUser" TEXT,
"liveEncryptedPassword" TEXT,
"liveContext" INTEGER,
"defaultNameServers" TEXT[] DEFAULT ARRAY[]::TEXT[],
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DomainsConfig_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DomainsCustomer" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"isOwnCompany" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DomainsCustomer_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DomainsContactAssignment" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"environment" "AutodnsEnvironment" NOT NULL,
"autodnsContactId" TEXT NOT NULL,
"customerId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DomainsContactAssignment_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DomainsOrder" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"environment" "AutodnsEnvironment" NOT NULL,
"domainName" TEXT NOT NULL,
"openKey" TEXT,
"status" "DomainOrderStatus" NOT NULL DEFAULT 'DRAFT',
"payload" JSONB NOT NULL,
"jobId" TEXT,
"jobStatus" TEXT,
"errorText" TEXT,
"createdByUserId" TEXT NOT NULL,
"confirmedByUserId" TEXT,
"confirmedByUsername" TEXT,
"confirmedAt" TIMESTAMP(3),
"lastCheckedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DomainsOrder_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "DomainsConfig_tenantId_key" ON "DomainsConfig"("tenantId");
-- CreateIndex
CREATE INDEX "DomainsConfig_tenantId_idx" ON "DomainsConfig"("tenantId");
-- CreateIndex
CREATE INDEX "DomainsCustomer_tenantId_idx" ON "DomainsCustomer"("tenantId");
-- CreateIndex
CREATE UNIQUE INDEX "DomainsCustomer_tenantId_name_key" ON "DomainsCustomer"("tenantId", "name");
-- CreateIndex
CREATE INDEX "DomainsContactAssignment_tenantId_idx" ON "DomainsContactAssignment"("tenantId");
-- CreateIndex
CREATE INDEX "DomainsContactAssignment_customerId_idx" ON "DomainsContactAssignment"("customerId");
-- CreateIndex
CREATE UNIQUE INDEX "DomainsContactAssignment_tenantId_environment_autodnsContac_key" ON "DomainsContactAssignment"("tenantId", "environment", "autodnsContactId");
-- CreateIndex
CREATE INDEX "DomainsOrder_tenantId_idx" ON "DomainsOrder"("tenantId");
-- CreateIndex
CREATE UNIQUE INDEX "DomainsOrder_tenantId_environment_openKey_key" ON "DomainsOrder"("tenantId", "environment", "openKey");
-- AddForeignKey
ALTER TABLE "DomainsContactAssignment" ADD CONSTRAINT "DomainsContactAssignment_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "DomainsCustomer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- Zeilenschutz
ALTER TABLE "DomainsConfig" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "DomainsConfig" FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_policy ON "DomainsConfig"
USING ("tenantId" = current_tenant_id());
ALTER TABLE "DomainsCustomer" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "DomainsCustomer" FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_policy ON "DomainsCustomer"
USING ("tenantId" = current_tenant_id());
ALTER TABLE "DomainsContactAssignment" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "DomainsContactAssignment" FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_policy ON "DomainsContactAssignment"
USING ("tenantId" = current_tenant_id());
ALTER TABLE "DomainsOrder" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "DomainsOrder" FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_policy ON "DomainsOrder"
USING ("tenantId" = current_tenant_id());
+102
View File
@@ -841,6 +841,108 @@ model NextcloudAlertSubscription {
@@index([tenantId, userId])
}
// Domains (quick-261008-dts): Anbindung an AutoDNS / InterNetX (Domainrobot).
// AutoDNS bleibt die Quelle der Wahrheit fuer Kontakte und Domains (werden live
// gelesen, nie gespiegelt); lokal liegt nur, was AutoDNS nicht kennt (Zuordnung
// Kontakt -> Kunde) oder was Geldsicherheit braucht (Bestellungen). AutoDNS-
// Kontakt- und Job-Ids sind opake Dezimalzahlen und stehen als String (kein
// Int32-Ueberlauf). Zeilenschutz nach Muster ProxmoxServer (tenantId, keine
// Relation zu Tenant).
enum AutodnsEnvironment {
DEMO
LIVE
}
enum DomainOrderStatus {
DRAFT
SUBMITTING
SUBMITTED
SUCCESS
FAILED
UNKNOWN
CANCELED
}
// Einstellungen je Organisation (Singleton). Getrennte Zugaenge fuer das
// Demo- und das Live-System; Passwoerter AES-verschluesselt (CryptoService),
// nie an den Client zurueckgegeben.
model DomainsConfig {
id String @id @default(uuid())
tenantId String @unique
environment AutodnsEnvironment @default(DEMO)
demoUser String?
demoEncryptedPassword String?
demoContext Int?
liveUser String?
liveEncryptedPassword String?
liveContext Int?
defaultNameServers String[] @default([])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([tenantId])
}
// Kunden, denen AutoDNS-Kontakte zugeordnet werden. Hoechstens einer ist die
// eigene Firma (Pruefung im Dienst). Kein Firmenname als Vorgabe.
model DomainsCustomer {
id String @id @default(uuid())
tenantId String
name String
isOwnCompany Boolean @default(false)
assignments DomainsContactAssignment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([tenantId, name])
@@index([tenantId])
}
// Zuordnung AutoDNS-Kontakt -> Kunde. Die Umgebung ist Teil des Schluessels,
// weil Kontakt-Ids von Demo- und Live-System zwangslaeufig kollidieren.
model DomainsContactAssignment {
id String @id @default(uuid())
tenantId String
environment AutodnsEnvironment
autodnsContactId String
customerId String
customer DomainsCustomer @relation(fields: [customerId], references: [id], onDelete: Restrict)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([tenantId, environment, autodnsContactId])
@@index([tenantId])
@@index([customerId])
}
// Domain-Bestellungen (Geldsicherheit). `openKey` traegt den Domainnamen,
// solange die Bestellung offen ist (DRAFT, SUBMITTING, SUBMITTED, UNKNOWN,
// SUCCESS), und ist bei FAILED/CANCELED null; die Unique-Regel darauf
// verhindert zwei offene Bestellungen derselben Domain (NULLs wiederholen
// sich in Postgres).
model DomainsOrder {
id String @id @default(uuid())
tenantId String
environment AutodnsEnvironment
domainName String
openKey String?
status DomainOrderStatus @default(DRAFT)
payload Json
jobId String?
jobStatus String?
errorText String?
createdByUserId String
confirmedByUserId String?
confirmedByUsername String?
confirmedAt DateTime?
lastCheckedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([tenantId, environment, openKey])
@@index([tenantId])
}
// Eigene Module (quick-260929-9wc): vom Administrator angelegte Seitenleisten-
// Eintraege, die eine externe https-Seite im Rahmen zeigen. Sichtbar fuer alle
// Benutzer des Mandanten. Zeilenschutz nach Muster ProxmoxServer (tenantId,