feat(api): API-Tokens, kurze Anfragen und OpenAPI für die CLI
- ApiToken (nur SHA-256), Bearer-Anmeldung, POST /auth/token für proxdeploy login, Verwaltung in den Einstellungen - anfrageErgaenzen: Storage, Bridge, Node, Benutzer und Schlüssel aus Server-Standards, Verbindungstest und Anmeldung – auch beim Template-Bau - Abschlussbox serverseitig (abschluss), Web und CLI zeigen dieselben Zeilen - Antwortschemas als zod, daraus /api/v1/openapi.json und cli/api/openapi.json (pnpm openapi) Refs #013 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
import { z } from "@/lib/validierung";
|
||||
|
||||
/**
|
||||
* Antwortformen der REST-API, soweit die CLI sie liest. Aus ihnen und den Anfrage-Schemas
|
||||
* entsteht `openapi.json` und daraus die Go-Typen der CLI (`cli/internal/api`).
|
||||
*
|
||||
* Objekte sind offen (`looseObject`): die API darf Felder ergänzen, ohne dass eine ältere CLI
|
||||
* bricht. Die Tests in `tests/openapi.test.ts` prüfen echte Antworten gegen diese Schemas.
|
||||
*/
|
||||
const Zeit = z.string().describe("ISO 8601");
|
||||
|
||||
export const FehlerAntwort = z.object({
|
||||
fehler: z.looseObject({
|
||||
code: z.string(),
|
||||
nachricht: z.string(),
|
||||
hinweis: z.string().optional(),
|
||||
felder: z.record(z.string(), z.string()).optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const Ich = z.looseObject({
|
||||
benutzername: z.string(),
|
||||
anzeigename: z.string(),
|
||||
rolle: z.enum(["ADMIN", "OPERATOR"]),
|
||||
rechte: z.array(z.string()),
|
||||
version: z.string(),
|
||||
});
|
||||
|
||||
export const NeuesToken = z.looseObject({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
praefix: z.string(),
|
||||
token: z.string(),
|
||||
erstelltAm: Zeit,
|
||||
ablaufAm: Zeit.nullable(),
|
||||
});
|
||||
|
||||
export const TokenAnmeldung = z.object({
|
||||
benutzername: z.string(),
|
||||
passwort: z.string(),
|
||||
name: z.string(),
|
||||
tage: z.number().int().optional(),
|
||||
});
|
||||
|
||||
export const ServerEintrag = z.looseObject({
|
||||
name: z.string(),
|
||||
titel: z.string(),
|
||||
url: z.string(),
|
||||
erreichbar: z.boolean().nullable(),
|
||||
pveVersion: z.string().nullable(),
|
||||
geprueftAm: Zeit.nullable(),
|
||||
sshAktiv: z.boolean(),
|
||||
letzterTest: z
|
||||
.looseObject({
|
||||
cluster: z.looseObject({ name: z.string() }).nullable(),
|
||||
knoten: z.array(
|
||||
z.looseObject({
|
||||
name: z.string(),
|
||||
online: z.boolean(),
|
||||
speicherBelegt: z.number().nullable(),
|
||||
speicherGesamt: z.number().nullable(),
|
||||
}),
|
||||
),
|
||||
})
|
||||
.nullable(),
|
||||
});
|
||||
|
||||
export const DistributionEintrag = z.looseObject({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
familie: z.string(),
|
||||
aktiv: z.boolean(),
|
||||
vm: z.boolean(),
|
||||
lxc: z.boolean(),
|
||||
});
|
||||
|
||||
export const ProfilVariable = z.looseObject({
|
||||
name: z.string(),
|
||||
label: z.string(),
|
||||
type: z.enum(["string", "number", "boolean", "select"]),
|
||||
required: z.boolean(),
|
||||
default: z.union([z.string(), z.number(), z.boolean()]).optional(),
|
||||
options: z.array(z.string()).optional(),
|
||||
help: z.string().optional(),
|
||||
});
|
||||
|
||||
export const ProfilEintrag = z.looseObject({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
beschreibung: z.string().nullable(),
|
||||
familien: z.array(z.string()),
|
||||
herkunft: z.enum(["KATALOG", "EIGEN"]),
|
||||
aktiv: z.boolean(),
|
||||
definition: z.looseObject({ variables: z.array(ProfilVariable).optional() }),
|
||||
});
|
||||
|
||||
export const VorlageEintrag = z.looseObject({
|
||||
name: z.string(),
|
||||
titel: z.string(),
|
||||
beschreibung: z.string().nullable(),
|
||||
});
|
||||
|
||||
export const TemplateEintrag = z.looseObject({
|
||||
vmid: z.number().int(),
|
||||
name: z.string(),
|
||||
knoten: z.string(),
|
||||
distributionId: z.string().nullable(),
|
||||
gebautAm: Zeit.nullable(),
|
||||
profile: z.array(z.string()),
|
||||
});
|
||||
|
||||
export const AuftragAngenommen = z.object({ id: z.string(), kurzId: z.string() });
|
||||
|
||||
const PhasenStatus = z.enum(["OFFEN", "LAEUFT", "ERLEDIGT", "FEHLER", "UEBERSPRUNGEN"]);
|
||||
|
||||
export const PhaseSicht = z.looseObject({
|
||||
schluessel: z.string(),
|
||||
titel: z.string(),
|
||||
status: PhasenStatus,
|
||||
detail: z.string().nullable(),
|
||||
ursache: z.string().nullable(),
|
||||
hinweis: z.string().nullable(),
|
||||
gestartetAm: Zeit.nullable().optional(),
|
||||
beendetAm: Zeit.nullable().optional(),
|
||||
});
|
||||
|
||||
/** Ergebnis eines fertigen Auftrags – Inhalt der Abschlussbox. */
|
||||
export const AuftragErgebnis = z.looseObject({
|
||||
vmid: z.number().int().optional(),
|
||||
knoten: z.string().optional(),
|
||||
ip: z.string().nullable().optional(),
|
||||
ssh: z.string().nullable().optional(),
|
||||
/** Nur beim Template-Bau: Name des Templates. */
|
||||
template: z.string().optional(),
|
||||
});
|
||||
|
||||
export const Abschluss = z.looseObject({
|
||||
ok: z.boolean(),
|
||||
titel: z.string(),
|
||||
dauer: z.string().nullable(),
|
||||
/** Je Zeile `[Bezeichnung, Wert]`. */
|
||||
zeilen: z.array(z.array(z.string()).length(2)),
|
||||
});
|
||||
|
||||
export const AuftragSicht = z.looseObject({
|
||||
id: z.string(),
|
||||
kurzId: z.string(),
|
||||
art: z.enum(["AUSROLLEN", "TEMPLATE_BAUEN", "AUFRAEUMEN"]),
|
||||
status: z.enum(["WARTEND", "LAEUFT", "ERFOLGREICH", "FEHLGESCHLAGEN", "ABGEBROCHEN", "AUFGERAEUMT"]),
|
||||
kopf: z.string(),
|
||||
server: z.looseObject({ name: z.string(), titel: z.string() }),
|
||||
knoten: z.string().nullable(),
|
||||
gastTyp: z.enum(["VM", "LXC"]).nullable(),
|
||||
vmid: z.number().int().nullable(),
|
||||
name: z.string().nullable(),
|
||||
distributionId: z.string().nullable(),
|
||||
profile: z.array(z.string()),
|
||||
ergebnis: AuftragErgebnis.nullable(),
|
||||
fehler: z
|
||||
.looseObject({ phase: z.string().optional(), ursache: z.string(), hinweis: z.string().optional() })
|
||||
.nullable(),
|
||||
erstelltVon: z.string(),
|
||||
erstelltAm: Zeit,
|
||||
gestartetAm: Zeit.nullable(),
|
||||
beendetAm: Zeit.nullable(),
|
||||
abschluss: Abschluss.nullable(),
|
||||
phasen: z.array(PhaseSicht),
|
||||
});
|
||||
|
||||
export const LogZeile = z.looseObject({
|
||||
id: z.string(),
|
||||
zeit: Zeit,
|
||||
stufe: z.string(),
|
||||
phase: z.string().nullable(),
|
||||
nachricht: z.string(),
|
||||
});
|
||||
|
||||
export const GeplanterAufruf = z.looseObject({
|
||||
methode: z.string(),
|
||||
pfad: z.string(),
|
||||
parameter: z.record(z.string(), z.unknown()).optional(),
|
||||
});
|
||||
|
||||
export const Plan = z.looseObject({
|
||||
kopf: z.string(),
|
||||
phasen: z.array(PhaseSicht),
|
||||
aufrufe: z.array(GeplanterAufruf),
|
||||
skript: z.string().nullable(),
|
||||
cliBefehl: z.string(),
|
||||
automatisch: z.array(z.string()),
|
||||
ok: z.boolean(),
|
||||
});
|
||||
@@ -20,6 +20,8 @@ export type AuftragSicht = {
|
||||
erstelltAm: string;
|
||||
gestartetAm: string | null;
|
||||
beendetAm: string | null;
|
||||
/** Abschlussbox, sobald der Auftrag fertig ist. */
|
||||
abschluss: Abschluss | null;
|
||||
phasen: Array<{
|
||||
schluessel: string;
|
||||
titel: string;
|
||||
@@ -32,4 +34,11 @@ export type AuftragSicht = {
|
||||
}>;
|
||||
};
|
||||
|
||||
export type Abschluss = {
|
||||
ok: boolean;
|
||||
titel: string;
|
||||
dauer: string | null;
|
||||
zeilen: Array<[string, string]>;
|
||||
};
|
||||
|
||||
export type LogZeile = { id: string; zeit: string; stufe: string; phase: string | null; nachricht: string };
|
||||
|
||||
Reference in New Issue
Block a user