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
@@ -0,0 +1,22 @@
-- CreateTable
CREATE TABLE "ApiToken" (
"id" TEXT NOT NULL,
"benutzerId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"praefix" TEXT NOT NULL,
"hash" TEXT NOT NULL,
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"zuletztGenutzt" TIMESTAMP(3),
"ablaufAm" TIMESTAMP(3),
CONSTRAINT "ApiToken_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "ApiToken_hash_key" ON "ApiToken"("hash");
-- CreateIndex
CREATE UNIQUE INDEX "ApiToken_benutzerId_name_key" ON "ApiToken"("benutzerId", "name");
-- AddForeignKey
ALTER TABLE "ApiToken" ADD CONSTRAINT "ApiToken_benutzerId_fkey" FOREIGN KEY ("benutzerId") REFERENCES "Benutzer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+36 -18
View File
@@ -24,22 +24,23 @@ enum Quelle {
}
model Benutzer {
id String @id @default(cuid())
benutzername String @unique
id String @id @default(cuid())
benutzername String @unique
anzeigename String
email String?
/// argon2id; leer bei Benutzern, die sich nur per OIDC anmelden.
passwortHash String?
oidcSubjekt String? @unique
rolle Rolle @default(OPERATOR)
aktiv Boolean @default(true)
oidcSubjekt String? @unique
rolle Rolle @default(OPERATOR)
aktiv Boolean @default(true)
/// Persönliche Standardwerte im Assistenten.
standards Json?
erstelltAm DateTime @default(now())
geaendertAm DateTime @updatedAt
erstelltAm DateTime @default(now())
geaendertAm DateTime @updatedAt
protokoll Protokoll[]
auftraege Auftrag[]
schluessel SshSchluessel[]
tokens ApiToken[]
}
/// Audit-Log. Nur anhängen – ein Trigger verweigert UPDATE und DELETE.
@@ -66,9 +67,9 @@ model Einstellung {
/// Ein Proxmox-VE-Einzelhost oder -Cluster. Geheimnisse liegen AES-256-GCM-verschlüsselt (v1.…).
model Server {
id String @id @default(cuid())
id String @id @default(cuid())
/// Kurzname, auch für die CLI (`--server rz1`).
name String @unique
name String @unique
/// Anzeigename, z. B. „Rechenzentrum 1“.
titel String
/// https://host:8006
@@ -77,19 +78,19 @@ model Server {
tokenId String
tokenGeheimnis String
/// Aus: nur ein gepinnter Fingerprint zählt. An: Systemvertrauen (+ optional Pin).
tlsPruefen Boolean @default(false)
tlsPruefen Boolean @default(false)
/// SHA-256 des Serverzertifikats, Großbuchstaben mit Doppelpunkten.
fingerabdruck String?
sshAktiv Boolean @default(false)
sshBenutzer String @default("root")
sshPort Int @default(22)
sshAktiv Boolean @default(false)
sshBenutzer String @default("root")
sshPort Int @default(22)
sshPrivat String?
sshOeffentlich String?
/// Gepinnte Host-Keys je Node: { "pve01": "SHA256:…" }.
sshHostSchluessel Json?
templateVmidVon Int @default(9000)
templateVmidBis Int @default(9999)
gastVmidVon Int @default(100)
templateVmidVon Int @default(9000)
templateVmidBis Int @default(9999)
gastVmidVon Int @default(100)
/// Statische Adresse für die Template-Vorbereitung ohne SSH: { bridge, vlan, ip, gateway }.
bauNetz Json?
/// Standard-Node, -Storage, -Bridge für den Assistenten.
@@ -99,8 +100,8 @@ model Server {
erreichbar Boolean?
pveVersion String?
geprueftAm DateTime?
erstelltAm DateTime @default(now())
geaendertAm DateTime @updatedAt
erstelltAm DateTime @default(now())
geaendertAm DateTime @updatedAt
auftraege Auftrag[]
templates Template[]
}
@@ -309,3 +310,20 @@ model SshSchluessel {
@@unique([benutzerId, fingerabdruck])
}
/// API-Token der CLI (`Authorization: Bearer pd_…`). Gespeichert wird nur der SHA-256 des Tokens –
/// es hat 256 Bit Zufall, ein langsamer Hash brächte nichts. Rolle kommt je Anfrage vom Benutzer.
model ApiToken {
id String @id @default(cuid())
benutzerId String
benutzer Benutzer @relation(fields: [benutzerId], references: [id], onDelete: Cascade)
name String
/// Die ersten Zeichen zur Wiedererkennung („pd_AbC1…“).
praefix String
hash String @unique
erstelltAm DateTime @default(now())
zuletztGenutzt DateTime?
ablaufAm DateTime?
@@unique([benutzerId, name])
}