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:
2026-10-12 15:08:27 +02:00
co-authored by Claude Opus 5.5
parent 1e195f9bca
commit 9fdced62db
27 changed files with 1386 additions and 58 deletions
+74
View File
@@ -0,0 +1,74 @@
import { beforeEach, describe, expect, it } from "vitest";
import { verschluesseln } from "@/lib/geheimnisse";
import * as A from "@/lib/schemas/api";
import { auftragAnlegen, auftragHolen } from "@/server/services/auftraege";
import { distributionenListe, katalogSynchronisieren, profileListe } from "@/server/services/katalog";
import { serverListe } from "@/server/services/server";
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
import { angemeldet } from "./helpers/anfragen";
/** Die Antwortschemas (Quelle der Go-Typen) müssen zu dem passen, was die Services wirklich liefern. */
describe.runIf(datenbankVerfuegbar)("Antwortschemas gegen echte Antworten", () => {
beforeEach(async () => {
await datenbankLeeren();
await katalogSynchronisieren();
});
it("Katalog, Server und Auftrag", async () => {
for (const d of await distributionenListe({ nurAktive: true })) A.DistributionEintrag.parse(d);
for (const p of await profileListe({ nurAktive: true })) A.ProfilEintrag.parse(p);
const server = await testPrisma.server.create({
data: {
name: "rz1",
titel: "RZ 1",
url: "https://127.0.0.1:8006",
tokenId: "a@pve!b",
tokenGeheimnis: verschluesseln("x"),
fingerabdruck: "AA",
},
});
for (const s of await serverListe()) A.ServerEintrag.parse(s);
const { benutzer } = await angemeldet("OPERATOR");
const auftrag = await auftragAnlegen(
{
art: "AUSROLLEN",
serverId: server.id,
knoten: "pve01",
gastTyp: "VM",
distributionId: "debian-13",
name: "web01",
profile: ["base", "docker"],
anfrage: {},
},
{ benutzer, quelle: "CLI" },
null,
async () => {},
);
A.AuftragSicht.parse(await auftragHolen(auftrag.kurzId));
await testPrisma.auftrag.update({
where: { id: auftrag.id },
data: {
status: "ERFOLGREICH",
vmid: 120,
gestartetAm: new Date(Date.now() - 114_000),
beendetAm: new Date(),
ergebnis: { vmid: 120, knoten: "pve01", ip: "10.0.20.15", ssh: "ssh florian@10.0.20.15" },
},
});
const fertig = A.AuftragSicht.parse(await auftragHolen(auftrag.kurzId));
expect(fertig.abschluss).toEqual({
ok: true,
titel: "web01 bereit",
dauer: "1m 54s",
zeilen: [
["VMID", "120"],
["IP", "10.0.20.15"],
["Profile", "base, docker"],
["SSH", "ssh florian@10.0.20.15"],
],
});
});
});