Files
tessera-ctl/apps/web/src/lib/domains-api.ts
T
schalli 2176f8ecce 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>
2026-10-08 12:37:21 +02:00

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' });
}