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:
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Domains — API-Client (quick-261008-dts). Konsumiert `/modules/domains/*`.
|
||||
* `credentials: 'include'` fuer Cookie-Auth, `NEXT_PUBLIC_API_URL` als Basis
|
||||
* (Muster `nextcloud-status-api.ts`). Der Browser spricht nie mit AutoDNS;
|
||||
* Zugangsdaten kommen hier nie zurueck (nur `hasPassword`).
|
||||
*/
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
|
||||
const BASE = '/modules/domains';
|
||||
|
||||
export type DomainsEnvironment = 'DEMO' | 'LIVE';
|
||||
|
||||
export interface DomainsStatus {
|
||||
environment: DomainsEnvironment;
|
||||
/** Ist das AKTIVE System eingerichtet? */
|
||||
configured: boolean;
|
||||
demoConfigured: boolean;
|
||||
liveConfigured: boolean;
|
||||
defaultNameServers: string[];
|
||||
}
|
||||
|
||||
export interface DomainsEnvironmentAccess {
|
||||
user: string | null;
|
||||
hasPassword: boolean;
|
||||
context: number | null;
|
||||
}
|
||||
|
||||
export interface DomainsSettings {
|
||||
environment: DomainsEnvironment;
|
||||
demo: DomainsEnvironmentAccess;
|
||||
live: DomainsEnvironmentAccess;
|
||||
defaultNameServers: string[];
|
||||
configured: { demo: boolean; live: boolean };
|
||||
}
|
||||
|
||||
/** Teilspeichern: nur mitgeschickte Felder aendern sich; leeres Passwort = unveraendert. */
|
||||
export interface SaveDomainsSettingsInput {
|
||||
environment?: DomainsEnvironment;
|
||||
confirmLive?: boolean;
|
||||
demoUser?: string;
|
||||
demoPassword?: string;
|
||||
demoContext?: number | null;
|
||||
liveUser?: string;
|
||||
livePassword?: string;
|
||||
liveContext?: number | null;
|
||||
defaultNameServers?: string[];
|
||||
}
|
||||
|
||||
export interface ConnectionTestResult {
|
||||
ok: boolean;
|
||||
kind?: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/** Fehler mit HTTP-Status, maschinenlesbarer Kennung und deutscher Servermeldung. */
|
||||
export class DomainsRequestError extends Error {
|
||||
constructor(
|
||||
readonly status: number,
|
||||
readonly code: string | null,
|
||||
message: string,
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'DomainsRequestError';
|
||||
}
|
||||
}
|
||||
|
||||
async function failure(res: Response): Promise<DomainsRequestError> {
|
||||
let message = `Request failed (${res.status})`;
|
||||
let code: string | null = null;
|
||||
try {
|
||||
const body = await res.json();
|
||||
const raw = body?.message;
|
||||
if (Array.isArray(raw)) message = raw.join(' ');
|
||||
else if (typeof raw === 'string') message = raw;
|
||||
if (typeof body?.code === 'string') code = body.code;
|
||||
} catch {
|
||||
// Antwort ohne JSON-Koerper — Standardmeldung bleibt.
|
||||
}
|
||||
return new DomainsRequestError(res.status, code, message);
|
||||
}
|
||||
|
||||
async function request<T>(
|
||||
path: string,
|
||||
init: { method?: string; json?: unknown } = {},
|
||||
): Promise<T> {
|
||||
const method = init.method ?? 'GET';
|
||||
const headers: Record<string, string> = {};
|
||||
let body: string | undefined;
|
||||
if (init.json !== undefined) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
body = JSON.stringify(init.json);
|
||||
}
|
||||
const res = await fetch(`${API_URL}${BASE}${path}`, {
|
||||
method,
|
||||
credentials: 'include',
|
||||
headers,
|
||||
body,
|
||||
...(method === 'GET' ? { cache: 'no-store' as const } : {}),
|
||||
});
|
||||
if (!res.ok) throw await failure(res);
|
||||
if (res.status === 204) return undefined as T;
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
|
||||
export function getDomainsStatus(): Promise<DomainsStatus> {
|
||||
return request<DomainsStatus>('/status');
|
||||
}
|
||||
|
||||
export function getDomainsSettings(): Promise<DomainsSettings> {
|
||||
return request<DomainsSettings>('/settings');
|
||||
}
|
||||
|
||||
export function saveDomainsSettings(input: SaveDomainsSettingsInput): Promise<DomainsSettings> {
|
||||
return request<DomainsSettings>('/settings', { method: 'PUT', json: input });
|
||||
}
|
||||
|
||||
export function testDomainsConnection(
|
||||
environment: DomainsEnvironment,
|
||||
): Promise<ConnectionTestResult> {
|
||||
return request<ConnectionTestResult>('/connection-test', {
|
||||
method: 'POST',
|
||||
json: { environment },
|
||||
});
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
* (`--tile`, `--tile-foreground`, `--primary`, `--primary-foreground`).
|
||||
*/
|
||||
|
||||
export type ModuleIconId = 'radar' | 'fuel' | 'certificate' | 'globe' | 'server' | 'utensils' | 'shopping-bag' | 'cloud' | 'tile';
|
||||
export type ModuleIconId = 'radar' | 'fuel' | 'certificate' | 'globe' | 'server' | 'utensils' | 'shopping-bag' | 'cloud' | 'earth' | 'tile';
|
||||
|
||||
const ICONS: Record<string, ModuleIconId> = {
|
||||
'tender-radar': 'radar',
|
||||
@@ -18,6 +18,7 @@ const ICONS: Record<string, ModuleIconId> = {
|
||||
'nextcloud-status': 'cloud',
|
||||
'kantine-datev': 'utensils',
|
||||
'handelsware-datev': 'shopping-bag',
|
||||
domains: 'earth',
|
||||
};
|
||||
|
||||
/** Symbol eines Moduls; unbekannte Module bekommen das allgemeine Kachel-Symbol. */
|
||||
|
||||
@@ -77,6 +77,12 @@ export const MODULE_REGISTRY: Record<string, ModuleRegistryEntry> = {
|
||||
{ ssr: false },
|
||||
),
|
||||
},
|
||||
domains: {
|
||||
component: dynamic(
|
||||
() => import('@/app/(portal)/modules/domains/page'),
|
||||
{ ssr: false },
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,6 +32,7 @@ const MODULE_TITLE_KEYS: Record<string, string> = {
|
||||
'tender-radar': 'tenderRadar.page.title',
|
||||
'kantine-datev': 'kantineDatev.title',
|
||||
'handelsware-datev': 'handelswareDatev.title',
|
||||
domains: 'domains.title',
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user