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:
+3
@@ -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";
|
||||
@@ -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 -------------------------------------------------------
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -48,7 +48,6 @@ function status(over: Partial<DomainsStatus> = {}): DomainsStatus {
|
||||
configured: true,
|
||||
demoConfigured: true,
|
||||
liveConfigured: true,
|
||||
defaultNameServers: [],
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<string[]>(padNameServers(settings.defaultNameServers));
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [message, setMessage] = useState<Message | null>(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 (
|
||||
<SettingsSection
|
||||
id="domains-nameservers"
|
||||
title={t('nameServers.title')}
|
||||
description={t('nameServers.description')}
|
||||
footer={
|
||||
<>
|
||||
<div className="mr-auto">
|
||||
<MessageLine message={message} />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={PRIMARY_BUTTON}
|
||||
disabled={saving || !dirty}
|
||||
onClick={save}
|
||||
>
|
||||
{saving ? t('saving') : t('save')}
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{rows.map((value, index) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: Zeilen sind positionsgebunden und haben keine Kennung
|
||||
<div key={index} className="flex items-end gap-2">
|
||||
<div className="flex-1 space-y-1">
|
||||
<label
|
||||
htmlFor={`domains-ns-${index}`}
|
||||
className="text-sm font-medium text-foreground"
|
||||
>
|
||||
{t('nameServers.label', { number: index + 1 })}
|
||||
</label>
|
||||
<input
|
||||
id={`domains-ns-${index}`}
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
maxLength={253}
|
||||
placeholder={`ns${index + 1}.example.com`}
|
||||
value={value}
|
||||
onChange={(e) => setRows(rows.map((r, i) => (i === index ? e.target.value : r)))}
|
||||
className={INPUT_CLASS}
|
||||
/>
|
||||
</div>
|
||||
{rows.length > MIN_NAMESERVER_ROWS && (
|
||||
<button
|
||||
type="button"
|
||||
className={SECONDARY_BUTTON}
|
||||
onClick={() => setRows(rows.filter((_, i) => i !== index))}
|
||||
>
|
||||
{t('nameServers.remove')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{rows.length < MAX_NAMESERVER_ROWS && (
|
||||
<button type="button" className={SECONDARY_BUTTON} onClick={() => setRows([...rows, ''])}>
|
||||
{t('nameServers.add')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Reiter ---------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -503,7 +396,6 @@ export function SettingsTab({ onChanged }: { onChanged: () => void }) {
|
||||
<SystemCard settings={settings} onSaved={handleSaved} />
|
||||
<AccessCard environment="DEMO" access={settings.demo} onSaved={handleSaved} />
|
||||
<AccessCard environment="LIVE" access={settings.live} onSaved={handleSaved} />
|
||||
<NameServersCard settings={settings} onSaved={handleSaved} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,7 +70,6 @@ function status(over: Partial<DomainsStatus> = {}): DomainsStatus {
|
||||
configured: true,
|
||||
demoConfigured: true,
|
||||
liveConfigured: false,
|
||||
defaultNameServers: [],
|
||||
...over,
|
||||
};
|
||||
}
|
||||
@@ -80,7 +79,6 @@ function settings(over: Partial<DomainsSettings> = {}): 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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user