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) <noreply@anthropic.com>
This commit is contained in:
2026-10-08 12:37:21 +02:00
parent 1b20b84e2f
commit 2176f8ecce
17 changed files with 22 additions and 241 deletions
@@ -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";
-1
View File
@@ -876,7 +876,6 @@ model DomainsConfig {
liveUser String?
liveEncryptedPassword String?
liveContext Int?
defaultNameServers String[] @default([])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -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> = {}): 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', () => {
@@ -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<DomainsSettingsView> {
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<string[]> {
return (await this.loadRow(tenantId))?.defaultNameServers ?? [];
}
// --- Verbindungstest -------------------------------------------------------
/**
-2
View File
@@ -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 {
@@ -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]);
});
@@ -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 {