- 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>
330 lines
9.9 KiB
Plaintext
330 lines
9.9 KiB
Plaintext
// Datenmodell von ProxDeploy. Namensabbildung zur Vorgabe: siehe CLAUDE.md.
|
||
// Modelle kommen mit dem Issue dazu, das sie braucht – der Plan in docs/architektur.md
|
||
// zeigt das Zielbild.
|
||
|
||
generator client {
|
||
provider = "prisma-client"
|
||
output = "../src/generated/prisma"
|
||
}
|
||
|
||
datasource db {
|
||
provider = "postgresql"
|
||
}
|
||
|
||
enum Rolle {
|
||
ADMIN
|
||
OPERATOR
|
||
}
|
||
|
||
enum Quelle {
|
||
WEB
|
||
CLI
|
||
API
|
||
SYSTEM
|
||
}
|
||
|
||
model Benutzer {
|
||
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)
|
||
/// Persönliche Standardwerte im Assistenten.
|
||
standards Json?
|
||
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.
|
||
model Protokoll {
|
||
id BigInt @id @default(autoincrement())
|
||
zeit DateTime @default(now())
|
||
benutzerId String?
|
||
benutzer Benutzer? @relation(fields: [benutzerId], references: [id], onDelete: SetNull)
|
||
quelle Quelle
|
||
aktion String
|
||
ziel String?
|
||
details Json?
|
||
ip String?
|
||
|
||
@@index([zeit])
|
||
@@index([benutzerId, zeit])
|
||
}
|
||
|
||
model Einstellung {
|
||
schluessel String @id
|
||
wert Json
|
||
geaendertAm DateTime @updatedAt
|
||
}
|
||
|
||
/// Ein Proxmox-VE-Einzelhost oder -Cluster. Geheimnisse liegen AES-256-GCM-verschlüsselt (v1.…).
|
||
model Server {
|
||
id String @id @default(cuid())
|
||
/// Kurzname, auch für die CLI (`--server rz1`).
|
||
name String @unique
|
||
/// Anzeigename, z. B. „Rechenzentrum 1“.
|
||
titel String
|
||
/// https://host:8006
|
||
url String
|
||
/// user@realm!tokenid
|
||
tokenId String
|
||
tokenGeheimnis String
|
||
/// Aus: nur ein gepinnter Fingerprint zählt. An: Systemvertrauen (+ optional Pin).
|
||
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)
|
||
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)
|
||
/// Statische Adresse für die Template-Vorbereitung ohne SSH: { bridge, vlan, ip, gateway }.
|
||
bauNetz Json?
|
||
/// Standard-Node, -Storage, -Bridge für den Assistenten.
|
||
standards Json?
|
||
/// Bericht des letzten Verbindungstests inkl. Fähigkeiten (VerbindungsBericht, #003).
|
||
letzterTest Json?
|
||
erreichbar Boolean?
|
||
pveVersion String?
|
||
geprueftAm DateTime?
|
||
erstelltAm DateTime @default(now())
|
||
geaendertAm DateTime @updatedAt
|
||
auftraege Auftrag[]
|
||
templates Template[]
|
||
}
|
||
|
||
enum Herkunft {
|
||
KATALOG
|
||
EIGEN
|
||
}
|
||
|
||
/// Spiegel von catalog/distros/*.yaml. Die Datei ist maßgeblich; nur `aktiv` gehört der Datenbank.
|
||
model Distribution {
|
||
id String @id
|
||
name String
|
||
familie String
|
||
/// Validierter Inhalt der YAML-Datei.
|
||
definition Json
|
||
quelltext String
|
||
dateiHash String
|
||
aktiv Boolean @default(true)
|
||
/// Gesetzt, wenn die Datei aus dem Katalog verschwunden ist – die Zeile bleibt für den Verlauf.
|
||
entferntAm DateTime?
|
||
synchronisiertAm DateTime
|
||
geaendertVonId String?
|
||
}
|
||
|
||
/// Profile aus catalog/profiles/*.yaml (KATALOG, schreibgeschützt) und eigene aus der GUI (EIGEN).
|
||
model Profil {
|
||
id String @id
|
||
name String
|
||
beschreibung String?
|
||
familien String[]
|
||
herkunft Herkunft
|
||
definition Json
|
||
/// YAML wie eingegeben – Kommentare bleiben erhalten.
|
||
quelltext String
|
||
dateiHash String?
|
||
aktiv Boolean @default(true)
|
||
entferntAm DateTime?
|
||
synchronisiertAm DateTime?
|
||
geaendertVonId String?
|
||
geaendertAm DateTime @updatedAt
|
||
}
|
||
|
||
enum GastTyp {
|
||
VM
|
||
LXC
|
||
}
|
||
|
||
enum AuftragsArt {
|
||
AUSROLLEN
|
||
TEMPLATE_BAUEN
|
||
AUFRAEUMEN
|
||
}
|
||
|
||
enum AuftragsStatus {
|
||
WARTEND
|
||
LAEUFT
|
||
ERFOLGREICH
|
||
FEHLGESCHLAGEN
|
||
ABGEBROCHEN
|
||
AUFGERAEUMT
|
||
}
|
||
|
||
enum PhasenStatus {
|
||
OFFEN
|
||
LAEUFT
|
||
ERLEDIGT
|
||
FEHLER
|
||
UEBERSPRUNGEN
|
||
}
|
||
|
||
/// Ein Hintergrundjob: Ausrollen, Template bauen oder Aufräumen. Läuft im worker.
|
||
model Auftrag {
|
||
id String @id @default(cuid())
|
||
/// Kurz und tippbar für CLI und URLs: „d-7f3k2“.
|
||
kurzId String @unique
|
||
art AuftragsArt
|
||
status AuftragsStatus @default(WARTEND)
|
||
serverId String
|
||
server Server @relation(fields: [serverId], references: [id], onDelete: Restrict)
|
||
knoten String?
|
||
gastTyp GastTyp?
|
||
vmid Int?
|
||
name String?
|
||
distributionId String?
|
||
profile String[]
|
||
/// Validierte Anfrage – Grundlage für „Wiederholen“.
|
||
anfrage Json
|
||
/// Plan zum Zeitpunkt des Starts (Phasen und geplante API-Aufrufe).
|
||
plan Json?
|
||
/// Kopfzeile wie in der CLI: „Server rz1 · Node pve01 · Debian 13 (Template 9013)“ – von der Engine gesetzt.
|
||
kopf String?
|
||
/// IP, SSH-Befehl, Dauer …
|
||
ergebnis Json?
|
||
/// Was ProxDeploy angelegt hat – Grundlage fürs Aufräumen.
|
||
angelegt Json?
|
||
/// { phase, ursache, hinweis }
|
||
fehler Json?
|
||
quelle Quelle
|
||
erstelltVonId String
|
||
erstelltVon Benutzer @relation(fields: [erstelltVonId], references: [id], onDelete: Restrict)
|
||
wiederholtVonId String?
|
||
/// Bei AUFRAEUMEN: der Auftrag, dessen Reste entfernt werden.
|
||
bezugId String?
|
||
abbrechen Boolean @default(false)
|
||
erstelltAm DateTime @default(now())
|
||
gestartetAm DateTime?
|
||
beendetAm DateTime?
|
||
phasen Phase[]
|
||
log Logeintrag[]
|
||
|
||
@@index([serverId, erstelltAm])
|
||
@@index([status])
|
||
@@index([erstelltAm])
|
||
}
|
||
|
||
model Phase {
|
||
id String @id @default(cuid())
|
||
auftragId String
|
||
auftrag Auftrag @relation(fields: [auftragId], references: [id], onDelete: Cascade)
|
||
reihenfolge Int
|
||
schluessel String
|
||
titel String
|
||
status PhasenStatus @default(OFFEN)
|
||
/// Der graue Detailtext hinter dem Phasennamen – in Web und CLI derselbe.
|
||
detail String?
|
||
ursache String?
|
||
hinweis String?
|
||
gestartetAm DateTime?
|
||
beendetAm DateTime?
|
||
|
||
@@unique([auftragId, schluessel])
|
||
@@index([auftragId, reihenfolge])
|
||
}
|
||
|
||
model Logeintrag {
|
||
/// Zugleich die Event-ID im SSE-Strom (Last-Event-ID).
|
||
id BigInt @id @default(autoincrement())
|
||
auftragId String
|
||
auftrag Auftrag @relation(fields: [auftragId], references: [id], onDelete: Cascade)
|
||
zeit DateTime @default(now())
|
||
stufe String
|
||
phase String?
|
||
nachricht String
|
||
daten Json?
|
||
|
||
@@index([auftragId, id])
|
||
}
|
||
|
||
/// Vergebene VMIDs, solange ein Auftrag läuft – verhindert Doppelvergabe zwischen parallelen Aufträgen.
|
||
model VmidReservierung {
|
||
serverId String
|
||
vmid Int
|
||
auftragId String
|
||
ablaufAm DateTime
|
||
|
||
@@id([serverId, vmid])
|
||
@@index([auftragId])
|
||
}
|
||
|
||
/// Von ProxDeploy gebaute Templates. Maßgeblich ist Proxmox (Tag proxdeploy-template); hier steht, woraus gebaut wurde.
|
||
model Template {
|
||
id String @id @default(cuid())
|
||
serverId String
|
||
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
|
||
knoten String
|
||
vmid Int
|
||
distributionId String
|
||
storage String
|
||
imageDatei String
|
||
pruefsumme String
|
||
/// Eingebackene Profile (#022), aufgelöst inkl. Voraussetzungen.
|
||
profile String[] @default([])
|
||
gebautVonAuftragId String?
|
||
gebautAm DateTime @default(now())
|
||
|
||
@@unique([serverId, vmid])
|
||
@@index([serverId, distributionId])
|
||
}
|
||
|
||
/// Vorlage (Preset): eine gespeicherte Kombination, die Assistent, CLI (--preset) und API vorbelegen.
|
||
model Vorlage {
|
||
/// Kurzname für die CLI: „docker-host-klein“
|
||
name String @id
|
||
titel String
|
||
beschreibung String?
|
||
/// Teilmenge der Ausroll-Anfrage (typ, distribution, cores, speicherMb, diskGb, profile, variablen, tags …).
|
||
parameter Json
|
||
erstelltVonId String?
|
||
erstelltAm DateTime @default(now())
|
||
geaendertAm DateTime @updatedAt
|
||
}
|
||
|
||
/// Öffentliche SSH-Schlüssel, die in Gäste kommen. benutzerId leer = Team-Schlüssel (in jeden Gast).
|
||
model SshSchluessel {
|
||
id String @id @default(cuid())
|
||
benutzerId String?
|
||
benutzer Benutzer? @relation(fields: [benutzerId], references: [id], onDelete: Cascade)
|
||
name String
|
||
oeffentlich String
|
||
/// SHA256:… wie ssh-keygen -lf
|
||
fingerabdruck String
|
||
/// Kennung der SchluesselQuelle („lokal“; später z. B. eine externe Schlüsselverwaltung).
|
||
quelle String @default("lokal")
|
||
erstelltAm DateTime @default(now())
|
||
|
||
@@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])
|
||
}
|