2176f8ecce
- 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>
360 lines
10 KiB
TypeScript
360 lines
10 KiB
TypeScript
/**
|
|
* 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;
|
|
}
|
|
|
|
export interface DomainsEnvironmentAccess {
|
|
user: string | null;
|
|
hasPassword: boolean;
|
|
context: number | null;
|
|
}
|
|
|
|
export interface DomainsSettings {
|
|
environment: DomainsEnvironment;
|
|
demo: DomainsEnvironmentAccess;
|
|
live: DomainsEnvironmentAccess;
|
|
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;
|
|
}
|
|
|
|
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 },
|
|
});
|
|
}
|
|
|
|
// --- Kunden, Kontakte und Domains (quick-261008-dts, Aufgabe 2) ----------------
|
|
|
|
export interface DomainsCustomer {
|
|
id: string;
|
|
name: string;
|
|
isOwnCompany: boolean;
|
|
/** Zugeordnete Kontakte im AKTIVEN System. */
|
|
contactCount: number;
|
|
}
|
|
|
|
export interface DomainsCustomerInput {
|
|
name: string;
|
|
isOwnCompany?: boolean;
|
|
}
|
|
|
|
export type DomainsContactType = 'PERSON' | 'ORG';
|
|
|
|
export interface DomainsContact {
|
|
/** AutoDNS-Kontakt-Nummer. */
|
|
id: number;
|
|
type: string | null;
|
|
displayName: string;
|
|
fname: string | null;
|
|
lname: string | null;
|
|
organization: string | null;
|
|
address: string[];
|
|
pcode: string | null;
|
|
city: string | null;
|
|
country: string | null;
|
|
email: string | null;
|
|
phone: string | null;
|
|
alias: string | null;
|
|
customerId: string | null;
|
|
customerName: string | null;
|
|
}
|
|
|
|
export interface DomainsContactList {
|
|
environment: DomainsEnvironment;
|
|
contacts: DomainsContact[];
|
|
/** Es werden nur die ersten 2000 Eintraege geliefert. */
|
|
truncated: boolean;
|
|
fetchedAt: string;
|
|
}
|
|
|
|
export interface DomainsDomain {
|
|
name: string;
|
|
/** ISO-Zeitpunkt oder null. */
|
|
expire: string | null;
|
|
/** Rohwert von AutoDNS (`registryStatus`). */
|
|
status: string | null;
|
|
cancelationPending: boolean;
|
|
ownerContactId: number | null;
|
|
ownerName: string | null;
|
|
customerId: string | null;
|
|
customerName: string | null;
|
|
}
|
|
|
|
export interface DomainsDomainList {
|
|
environment: DomainsEnvironment;
|
|
domains: DomainsDomain[];
|
|
truncated: boolean;
|
|
fetchedAt: string;
|
|
}
|
|
|
|
export interface CreateDomainsContactInput {
|
|
type: DomainsContactType;
|
|
organization?: string;
|
|
fname: string;
|
|
lname: string;
|
|
/** Anschriftzeilen (Strasse und Hausnummer, optional eine zweite Zeile). */
|
|
street: string[];
|
|
pcode: string;
|
|
city: string;
|
|
/** Laendercode nach ISO 3166-1 (zwei Grossbuchstaben). */
|
|
country: string;
|
|
email: string;
|
|
phone: string;
|
|
customerId?: string;
|
|
}
|
|
|
|
export function listCustomers(): Promise<DomainsCustomer[]> {
|
|
return request<DomainsCustomer[]>('/customers');
|
|
}
|
|
|
|
export function createCustomer(input: DomainsCustomerInput): Promise<DomainsCustomer> {
|
|
return request<DomainsCustomer>('/customers', { method: 'POST', json: input });
|
|
}
|
|
|
|
export function updateCustomer(id: string, input: DomainsCustomerInput): Promise<DomainsCustomer> {
|
|
return request<DomainsCustomer>(`/customers/${encodeURIComponent(id)}`, {
|
|
method: 'PUT',
|
|
json: input,
|
|
});
|
|
}
|
|
|
|
export function deleteCustomer(id: string): Promise<{ deleted: true }> {
|
|
return request<{ deleted: true }>(`/customers/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
}
|
|
|
|
export function listContacts(opts: { refresh?: boolean } = {}): Promise<DomainsContactList> {
|
|
return request<DomainsContactList>(`/contacts${opts.refresh ? '?refresh=1' : ''}`);
|
|
}
|
|
|
|
export function createContact(input: CreateDomainsContactInput): Promise<DomainsContact> {
|
|
return request<DomainsContact>('/contacts', { method: 'POST', json: input });
|
|
}
|
|
|
|
/** `customerId: null` loest die Zuordnung. */
|
|
export function assignContacts(
|
|
contactIds: number[],
|
|
customerId: string | null,
|
|
): Promise<{ assigned: number; removed: number }> {
|
|
return request<{ assigned: number; removed: number }>('/contacts/assign', {
|
|
method: 'POST',
|
|
json: { contactIds, customerId },
|
|
});
|
|
}
|
|
|
|
export function listDomains(opts: { refresh?: boolean } = {}): Promise<DomainsDomainList> {
|
|
return request<DomainsDomainList>(`/domains${opts.refresh ? '?refresh=1' : ''}`);
|
|
}
|
|
|
|
// --- Verfuegbarkeit, Registrierung und Auftraege (quick-261008-dts, Aufgabe 3) ----
|
|
|
|
export type DomainOrderStatus =
|
|
| 'DRAFT'
|
|
| 'SUBMITTING'
|
|
| 'SUBMITTED'
|
|
| 'SUCCESS'
|
|
| 'FAILED'
|
|
| 'UNKNOWN'
|
|
| 'CANCELED';
|
|
|
|
export interface OrderPrice {
|
|
amount: number;
|
|
currency: string;
|
|
}
|
|
|
|
export interface AvailabilityResult {
|
|
environment: DomainsEnvironment;
|
|
/** ASCII-Schreibweise (Punycode) — so geht sie an AutoDNS. */
|
|
domain: string;
|
|
domainUnicode: string;
|
|
/** Nur `FREE` ist bestellbar. */
|
|
available: boolean;
|
|
whoisStatus: string;
|
|
/** Preis fuer ein Jahr; `null`, wenn AutoDNS keinen nennt. */
|
|
price: OrderPrice | null;
|
|
}
|
|
|
|
export interface DomainsOrder {
|
|
id: string;
|
|
environment: DomainsEnvironment;
|
|
domainName: string;
|
|
domainNameUnicode: string;
|
|
status: DomainOrderStatus;
|
|
jobStatus: string | null;
|
|
errorText: string | null;
|
|
confirmedByUsername: string | null;
|
|
confirmedAt: string | null;
|
|
createdAt: string;
|
|
lastCheckedAt: string | null;
|
|
price: OrderPrice | null;
|
|
}
|
|
|
|
export interface OrderContact {
|
|
id: number;
|
|
displayName: string;
|
|
}
|
|
|
|
/** Zusammenfassung eines Entwurfs — das, was der Benutzer verbindlich bestaetigt. */
|
|
export interface OrderSummary {
|
|
order: DomainsOrder;
|
|
contacts: {
|
|
owner: OrderContact;
|
|
admin: OrderContact;
|
|
tech: OrderContact;
|
|
zone: OrderContact;
|
|
};
|
|
nameServers: string[];
|
|
periodYears: 1;
|
|
/** Stand des Entwurfs; geht beim verbindlichen Bestellen unveraendert zurueck. */
|
|
version: string;
|
|
}
|
|
|
|
export interface CreateOrderInput {
|
|
domain: string;
|
|
ownerContactId: number;
|
|
adminContactId: number;
|
|
techContactId: number;
|
|
zoneContactId: number;
|
|
// Die Nameserver liest der Server selbst aus dem AutoDNS-Profil (h3t).
|
|
}
|
|
|
|
/** Standard-Nameserver aus dem AutoDNS-Benutzerprofil, in der Reihenfolge von AutoDNS. */
|
|
export interface DomainsNameServers {
|
|
environment: DomainsEnvironment;
|
|
nameServers: string[];
|
|
}
|
|
|
|
/** Liest die Standard-Nameserver aus AutoDNS (nur zur Anzeige; der Server liest sie beim Entwurf neu). */
|
|
export function getNameServers(): Promise<DomainsNameServers> {
|
|
return request<DomainsNameServers>('/name-servers');
|
|
}
|
|
|
|
export function checkAvailability(domain: string): Promise<AvailabilityResult> {
|
|
return request<AvailabilityResult>('/availability', { method: 'POST', json: { domain } });
|
|
}
|
|
|
|
export function createOrder(input: CreateOrderInput): Promise<OrderSummary> {
|
|
return request<OrderSummary>('/orders', { method: 'POST', json: input });
|
|
}
|
|
|
|
/**
|
|
* Verbindlich: schickt die Registrierung an AutoDNS (kostet im Live-System Geld).
|
|
* `version` ist der Stand aus der Zusammenfassung; hat sich der Entwurf seither
|
|
* geaendert, lehnt der Server mit 409 ab.
|
|
*/
|
|
export function submitOrder(id: string, version: string): Promise<DomainsOrder> {
|
|
return request<DomainsOrder>(`/orders/${encodeURIComponent(id)}/submit`, {
|
|
method: 'POST',
|
|
json: { version },
|
|
});
|
|
}
|
|
|
|
export function cancelOrder(id: string): Promise<DomainsOrder> {
|
|
return request<DomainsOrder>(`/orders/${encodeURIComponent(id)}/cancel`, { method: 'POST' });
|
|
}
|
|
|
|
export function listOrders(): Promise<DomainsOrder[]> {
|
|
return request<DomainsOrder[]>('/orders');
|
|
}
|
|
|
|
/** Gleicht offene Auftraege mit AutoDNS ab und liefert die aktuelle Liste. */
|
|
export function refreshOrders(): Promise<DomainsOrder[]> {
|
|
return request<DomainsOrder[]>('/orders/refresh', { method: 'POST' });
|
|
}
|