feat(db): Datenmodell, erste Migration und Seed (#003)
Prisma 7.10 mit Postgres-Adapter, 23 Modelle inklusive Rechnungen mit Positionen, Seed fuer Administrator, Warnregeln und Demo-Daten, Testdatenbank fuer Vitest. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,580 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "Produkt" AS ENUM ('PVE', 'PBS', 'PMG');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SubscriptionStatus" AS ENUM ('ACTIVE', 'NOTFOUND', 'INVALID', 'EXPIRED', 'SUSPENDED', 'UNKNOWN');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "LaufzeitStatus" AS ENUM ('VORSCHLAG', 'BESTAETIGT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AlertSchwere" AS ENUM ('INFO', 'WARNUNG', 'GEFAHR');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "NotificationKanal" AS ENUM ('EMAIL', 'WEBHOOK');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "NotificationStatus" AS ENUM ('GESENDET', 'FEHLGESCHLAGEN', 'WARTEND');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Kunde" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"kundennummer" TEXT NOT NULL,
|
||||
"ansprechpartner" TEXT,
|
||||
"email" TEXT,
|
||||
"telefon" TEXT,
|
||||
"notizen" TEXT,
|
||||
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Kunde_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Standort" (
|
||||
"id" TEXT NOT NULL,
|
||||
"kundeId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"strasse" TEXT,
|
||||
"plz" TEXT,
|
||||
"ort" TEXT,
|
||||
"notizen" TEXT,
|
||||
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Standort_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Host" (
|
||||
"id" TEXT NOT NULL,
|
||||
"agentId" TEXT NOT NULL,
|
||||
"hostname" TEXT NOT NULL,
|
||||
"fqdn" TEXT,
|
||||
"kundeId" TEXT,
|
||||
"standortId" TEXT,
|
||||
"produkt" "Produkt" NOT NULL,
|
||||
"produktVersion" TEXT,
|
||||
"kernel" TEXT,
|
||||
"serverId" TEXT,
|
||||
"clusterName" TEXT,
|
||||
"nodeName" TEXT,
|
||||
"ips" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||
"sockets" INTEGER,
|
||||
"cpuModell" TEXT,
|
||||
"uptimeSekunden" BIGINT,
|
||||
"paketUpdates" INTEGER,
|
||||
"agentVersion" TEXT,
|
||||
"notizen" TEXT,
|
||||
"subKey" TEXT,
|
||||
"subStatus" "SubscriptionStatus" NOT NULL DEFAULT 'UNKNOWN',
|
||||
"subLevel" TEXT,
|
||||
"subProduktname" TEXT,
|
||||
"subLizenzierteSockets" INTEGER,
|
||||
"subRegdate" DATE,
|
||||
"subNextduedate" DATE,
|
||||
"subChecktime" TIMESTAMP(3),
|
||||
"erstmalsGesehenAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"letzterReportAm" TIMESTAMP(3),
|
||||
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Host_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Tag" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"farbe" TEXT,
|
||||
|
||||
CONSTRAINT "Tag_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Report" (
|
||||
"id" TEXT NOT NULL,
|
||||
"hostId" TEXT NOT NULL,
|
||||
"reportId" TEXT NOT NULL,
|
||||
"erhobenAm" TIMESTAMP(3) NOT NULL,
|
||||
"empfangenAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"subStatus" "SubscriptionStatus" NOT NULL,
|
||||
"subKey" TEXT,
|
||||
"subNextduedate" DATE,
|
||||
"serverId" TEXT,
|
||||
"hostname" TEXT,
|
||||
"produktVersion" TEXT,
|
||||
"kernel" TEXT,
|
||||
"sockets" INTEGER,
|
||||
"paketUpdates" INTEGER,
|
||||
"agentVersion" TEXT,
|
||||
"ips" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||
"quellIp" TEXT,
|
||||
"rohdaten" JSONB NOT NULL,
|
||||
|
||||
CONSTRAINT "Report_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "SubscriptionKeyHistorie" (
|
||||
"id" TEXT NOT NULL,
|
||||
"hostId" TEXT NOT NULL,
|
||||
"key" TEXT NOT NULL,
|
||||
"ersteMeldung" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"letzteMeldung" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "SubscriptionKeyHistorie_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ServerIdHistorie" (
|
||||
"id" TEXT NOT NULL,
|
||||
"hostId" TEXT NOT NULL,
|
||||
"serverId" TEXT NOT NULL,
|
||||
"ersteMeldung" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"letzteMeldung" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "ServerIdHistorie_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Laufzeit" (
|
||||
"id" TEXT NOT NULL,
|
||||
"hostId" TEXT NOT NULL,
|
||||
"key" TEXT NOT NULL,
|
||||
"start" DATE NOT NULL,
|
||||
"ende" DATE NOT NULL,
|
||||
"status" "LaufzeitStatus" NOT NULL DEFAULT 'VORSCHLAG',
|
||||
"notizen" TEXT,
|
||||
"proxmoxPositionId" TEXT,
|
||||
"kundenPositionId" TEXT,
|
||||
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Laufzeit_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ProxmoxRechnung" (
|
||||
"id" TEXT NOT NULL,
|
||||
"nummer" TEXT NOT NULL,
|
||||
"datum" DATE NOT NULL,
|
||||
"faelligAm" DATE,
|
||||
"nettoBetrag" DECIMAL(12,2) NOT NULL,
|
||||
"ustSatz" DECIMAL(5,2) NOT NULL DEFAULT 19.00,
|
||||
"rabatt" DECIMAL(12,2),
|
||||
"zahlungsart" TEXT,
|
||||
"bezahlt" BOOLEAN NOT NULL DEFAULT false,
|
||||
"bezahltAm" DATE,
|
||||
"notizen" TEXT,
|
||||
"anhangId" TEXT,
|
||||
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "ProxmoxRechnung_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ProxmoxRechnungPosition" (
|
||||
"id" TEXT NOT NULL,
|
||||
"rechnungId" TEXT NOT NULL,
|
||||
"bezeichnung" TEXT NOT NULL,
|
||||
"menge" INTEGER NOT NULL DEFAULT 1,
|
||||
"nettoBetrag" DECIMAL(12,2) NOT NULL,
|
||||
|
||||
CONSTRAINT "ProxmoxRechnungPosition_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Kundenrechnung" (
|
||||
"id" TEXT NOT NULL,
|
||||
"kundeId" TEXT NOT NULL,
|
||||
"erstellt" BOOLEAN NOT NULL DEFAULT false,
|
||||
"nummer" TEXT,
|
||||
"datum" DATE,
|
||||
"nettoBetrag" DECIMAL(12,2),
|
||||
"ustSatz" DECIMAL(5,2) NOT NULL DEFAULT 19.00,
|
||||
"bezahlt" BOOLEAN NOT NULL DEFAULT false,
|
||||
"bezahltAm" DATE,
|
||||
"notizen" TEXT,
|
||||
"anhangId" TEXT,
|
||||
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Kundenrechnung_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "KundenrechnungPosition" (
|
||||
"id" TEXT NOT NULL,
|
||||
"rechnungId" TEXT NOT NULL,
|
||||
"bezeichnung" TEXT NOT NULL,
|
||||
"menge" INTEGER NOT NULL DEFAULT 1,
|
||||
"nettoBetrag" DECIMAL(12,2) NOT NULL,
|
||||
|
||||
CONSTRAINT "KundenrechnungPosition_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Anhang" (
|
||||
"id" TEXT NOT NULL,
|
||||
"dateiname" TEXT NOT NULL,
|
||||
"pfad" TEXT NOT NULL,
|
||||
"mimeTyp" TEXT NOT NULL,
|
||||
"groesse" INTEGER NOT NULL,
|
||||
"sha256" TEXT NOT NULL,
|
||||
"hochgeladenAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Anhang_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AgentToken" (
|
||||
"id" TEXT NOT NULL,
|
||||
"hostId" TEXT NOT NULL,
|
||||
"tokenHash" TEXT NOT NULL,
|
||||
"praefix" TEXT NOT NULL,
|
||||
"bezeichnung" TEXT,
|
||||
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"letzteNutzungAm" TIMESTAMP(3),
|
||||
"widerrufenAm" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "AgentToken_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "EnrollmentToken" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tokenHash" TEXT NOT NULL,
|
||||
"praefix" TEXT NOT NULL,
|
||||
"bezeichnung" TEXT,
|
||||
"gueltigBis" TIMESTAMP(3),
|
||||
"maxNutzungen" INTEGER,
|
||||
"nutzungen" INTEGER NOT NULL DEFAULT 0,
|
||||
"kundeId" TEXT,
|
||||
"standortId" TEXT,
|
||||
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"widerrufenAm" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "EnrollmentToken_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Alert" (
|
||||
"id" TEXT NOT NULL,
|
||||
"regel" TEXT NOT NULL,
|
||||
"fingerprint" TEXT NOT NULL,
|
||||
"schwere" "AlertSchwere" NOT NULL,
|
||||
"titel" TEXT NOT NULL,
|
||||
"beschreibung" TEXT NOT NULL,
|
||||
"hostId" TEXT,
|
||||
"kundeId" TEXT,
|
||||
"laufzeitId" TEXT,
|
||||
"geoeffnetAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"quittiertAm" TIMESTAMP(3),
|
||||
"erledigtAm" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "Alert_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Benachrichtigung" (
|
||||
"id" TEXT NOT NULL,
|
||||
"alertId" TEXT,
|
||||
"kanal" "NotificationKanal" NOT NULL,
|
||||
"empfaenger" TEXT NOT NULL,
|
||||
"betreff" TEXT,
|
||||
"status" "NotificationStatus" NOT NULL DEFAULT 'WARTEND',
|
||||
"fehler" TEXT,
|
||||
"versendetAm" TIMESTAMP(3),
|
||||
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Benachrichtigung_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "WarnregelEinstellung" (
|
||||
"id" TEXT NOT NULL,
|
||||
"regel" TEXT NOT NULL,
|
||||
"aktiv" BOOLEAN NOT NULL DEFAULT true,
|
||||
"schwellwerte" JSONB NOT NULL DEFAULT '{}',
|
||||
"geaendertAm" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "WarnregelEinstellung_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Benachrichtigungskanal" (
|
||||
"id" TEXT NOT NULL,
|
||||
"typ" "NotificationKanal" NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"aktiv" BOOLEAN NOT NULL DEFAULT true,
|
||||
"konfiguration" JSONB NOT NULL,
|
||||
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Benachrichtigungskanal_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Benutzer" (
|
||||
"id" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"passwortHash" TEXT,
|
||||
"oidcSubject" TEXT,
|
||||
"aktiv" BOOLEAN NOT NULL DEFAULT true,
|
||||
"letzterLoginAm" TIMESTAMP(3),
|
||||
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Benutzer_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AuditEintrag" (
|
||||
"id" TEXT NOT NULL,
|
||||
"benutzerId" TEXT,
|
||||
"aktion" TEXT NOT NULL,
|
||||
"entitaet" TEXT NOT NULL,
|
||||
"entitaetId" TEXT,
|
||||
"aenderung" JSONB,
|
||||
"ipAdresse" TEXT,
|
||||
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "AuditEintrag_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Einstellung" (
|
||||
"schluessel" TEXT NOT NULL,
|
||||
"wert" JSONB NOT NULL,
|
||||
"geaendertAm" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Einstellung_pkey" PRIMARY KEY ("schluessel")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_HostTags" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "_HostTags_AB_pkey" PRIMARY KEY ("A","B")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Kunde_kundennummer_key" ON "Kunde"("kundennummer");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Kunde_name_idx" ON "Kunde"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Standort_kundeId_idx" ON "Standort"("kundeId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Host_agentId_key" ON "Host"("agentId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Host_kundeId_idx" ON "Host"("kundeId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Host_standortId_idx" ON "Host"("standortId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Host_hostname_idx" ON "Host"("hostname");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Host_serverId_idx" ON "Host"("serverId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Host_subKey_idx" ON "Host"("subKey");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Host_subNextduedate_idx" ON "Host"("subNextduedate");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Host_subStatus_idx" ON "Host"("subStatus");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Tag_name_key" ON "Tag"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Report_reportId_key" ON "Report"("reportId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Report_hostId_erhobenAm_idx" ON "Report"("hostId", "erhobenAm");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Report_empfangenAm_idx" ON "Report"("empfangenAm");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "SubscriptionKeyHistorie_key_idx" ON "SubscriptionKeyHistorie"("key");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "SubscriptionKeyHistorie_hostId_key_key" ON "SubscriptionKeyHistorie"("hostId", "key");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ServerIdHistorie_serverId_idx" ON "ServerIdHistorie"("serverId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ServerIdHistorie_hostId_serverId_key" ON "ServerIdHistorie"("hostId", "serverId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Laufzeit_proxmoxPositionId_key" ON "Laufzeit"("proxmoxPositionId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Laufzeit_kundenPositionId_key" ON "Laufzeit"("kundenPositionId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Laufzeit_ende_idx" ON "Laufzeit"("ende");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Laufzeit_key_idx" ON "Laufzeit"("key");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Laufzeit_hostId_start_key" ON "Laufzeit"("hostId", "start");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ProxmoxRechnung_nummer_key" ON "ProxmoxRechnung"("nummer");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ProxmoxRechnung_bezahlt_faelligAm_idx" ON "ProxmoxRechnung"("bezahlt", "faelligAm");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ProxmoxRechnung_datum_idx" ON "ProxmoxRechnung"("datum");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ProxmoxRechnungPosition_rechnungId_idx" ON "ProxmoxRechnungPosition"("rechnungId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Kundenrechnung_nummer_key" ON "Kundenrechnung"("nummer");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Kundenrechnung_kundeId_idx" ON "Kundenrechnung"("kundeId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Kundenrechnung_erstellt_bezahlt_idx" ON "Kundenrechnung"("erstellt", "bezahlt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "KundenrechnungPosition_rechnungId_idx" ON "KundenrechnungPosition"("rechnungId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AgentToken_tokenHash_key" ON "AgentToken"("tokenHash");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AgentToken_hostId_idx" ON "AgentToken"("hostId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "EnrollmentToken_tokenHash_key" ON "EnrollmentToken"("tokenHash");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Alert_fingerprint_key" ON "Alert"("fingerprint");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Alert_erledigtAm_idx" ON "Alert"("erledigtAm");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Alert_hostId_idx" ON "Alert"("hostId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Alert_kundeId_idx" ON "Alert"("kundeId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Alert_regel_idx" ON "Alert"("regel");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Benachrichtigung_alertId_idx" ON "Benachrichtigung"("alertId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Benachrichtigung_erstelltAm_idx" ON "Benachrichtigung"("erstelltAm");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "WarnregelEinstellung_regel_key" ON "WarnregelEinstellung"("regel");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Benutzer_email_key" ON "Benutzer"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Benutzer_oidcSubject_key" ON "Benutzer"("oidcSubject");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AuditEintrag_entitaet_entitaetId_idx" ON "AuditEintrag"("entitaet", "entitaetId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AuditEintrag_erstelltAm_idx" ON "AuditEintrag"("erstelltAm");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_HostTags_B_index" ON "_HostTags"("B");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Standort" ADD CONSTRAINT "Standort_kundeId_fkey" FOREIGN KEY ("kundeId") REFERENCES "Kunde"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Host" ADD CONSTRAINT "Host_kundeId_fkey" FOREIGN KEY ("kundeId") REFERENCES "Kunde"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Host" ADD CONSTRAINT "Host_standortId_fkey" FOREIGN KEY ("standortId") REFERENCES "Standort"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Report" ADD CONSTRAINT "Report_hostId_fkey" FOREIGN KEY ("hostId") REFERENCES "Host"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SubscriptionKeyHistorie" ADD CONSTRAINT "SubscriptionKeyHistorie_hostId_fkey" FOREIGN KEY ("hostId") REFERENCES "Host"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ServerIdHistorie" ADD CONSTRAINT "ServerIdHistorie_hostId_fkey" FOREIGN KEY ("hostId") REFERENCES "Host"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Laufzeit" ADD CONSTRAINT "Laufzeit_hostId_fkey" FOREIGN KEY ("hostId") REFERENCES "Host"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Laufzeit" ADD CONSTRAINT "Laufzeit_proxmoxPositionId_fkey" FOREIGN KEY ("proxmoxPositionId") REFERENCES "ProxmoxRechnungPosition"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Laufzeit" ADD CONSTRAINT "Laufzeit_kundenPositionId_fkey" FOREIGN KEY ("kundenPositionId") REFERENCES "KundenrechnungPosition"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ProxmoxRechnung" ADD CONSTRAINT "ProxmoxRechnung_anhangId_fkey" FOREIGN KEY ("anhangId") REFERENCES "Anhang"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ProxmoxRechnungPosition" ADD CONSTRAINT "ProxmoxRechnungPosition_rechnungId_fkey" FOREIGN KEY ("rechnungId") REFERENCES "ProxmoxRechnung"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Kundenrechnung" ADD CONSTRAINT "Kundenrechnung_kundeId_fkey" FOREIGN KEY ("kundeId") REFERENCES "Kunde"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Kundenrechnung" ADD CONSTRAINT "Kundenrechnung_anhangId_fkey" FOREIGN KEY ("anhangId") REFERENCES "Anhang"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "KundenrechnungPosition" ADD CONSTRAINT "KundenrechnungPosition_rechnungId_fkey" FOREIGN KEY ("rechnungId") REFERENCES "Kundenrechnung"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AgentToken" ADD CONSTRAINT "AgentToken_hostId_fkey" FOREIGN KEY ("hostId") REFERENCES "Host"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "EnrollmentToken" ADD CONSTRAINT "EnrollmentToken_kundeId_fkey" FOREIGN KEY ("kundeId") REFERENCES "Kunde"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "EnrollmentToken" ADD CONSTRAINT "EnrollmentToken_standortId_fkey" FOREIGN KEY ("standortId") REFERENCES "Standort"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Alert" ADD CONSTRAINT "Alert_hostId_fkey" FOREIGN KEY ("hostId") REFERENCES "Host"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Alert" ADD CONSTRAINT "Alert_kundeId_fkey" FOREIGN KEY ("kundeId") REFERENCES "Kunde"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Alert" ADD CONSTRAINT "Alert_laufzeitId_fkey" FOREIGN KEY ("laufzeitId") REFERENCES "Laufzeit"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Benachrichtigung" ADD CONSTRAINT "Benachrichtigung_alertId_fkey" FOREIGN KEY ("alertId") REFERENCES "Alert"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AuditEintrag" ADD CONSTRAINT "AuditEintrag_benutzerId_fkey" FOREIGN KEY ("benutzerId") REFERENCES "Benutzer"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_HostTags" ADD CONSTRAINT "_HostTags_A_fkey" FOREIGN KEY ("A") REFERENCES "Host"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_HostTags" ADD CONSTRAINT "_HostTags_B_fkey" FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
@@ -0,0 +1,484 @@
|
||||
// ProxLedger – Datenmodell
|
||||
// Entwurfsentscheidungen siehe docs/architektur.md, Abschnitt 3.
|
||||
// Kurz: Ein Host wird über seine agentId identifiziert (nicht über die Server-ID, die sich bei
|
||||
// Hardwaretausch ändert), Rechnungen sind eigene Entitäten mit Positionen, damit Sammelbestellungen
|
||||
// bei Proxmox und Sammelrechnungen an Kunden ohne doppelte Rechnungsnummern abbildbar sind.
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client"
|
||||
output = "../src/generated/prisma"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Aufzählungen
|
||||
|
||||
enum Produkt {
|
||||
PVE
|
||||
PBS
|
||||
PMG
|
||||
}
|
||||
|
||||
enum SubscriptionStatus {
|
||||
ACTIVE
|
||||
NOTFOUND
|
||||
INVALID
|
||||
EXPIRED
|
||||
SUSPENDED
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
enum LaufzeitStatus {
|
||||
VORSCHLAG
|
||||
BESTAETIGT
|
||||
}
|
||||
|
||||
enum AlertSchwere {
|
||||
INFO
|
||||
WARNUNG
|
||||
GEFAHR
|
||||
}
|
||||
|
||||
enum NotificationKanal {
|
||||
EMAIL
|
||||
WEBHOOK
|
||||
}
|
||||
|
||||
enum NotificationStatus {
|
||||
GESENDET
|
||||
FEHLGESCHLAGEN
|
||||
WARTEND
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Kunden und Standorte
|
||||
|
||||
model Kunde {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
kundennummer String @unique
|
||||
ansprechpartner String?
|
||||
email String?
|
||||
telefon String?
|
||||
notizen String?
|
||||
erstelltAm DateTime @default(now())
|
||||
geaendertAm DateTime @updatedAt
|
||||
|
||||
standorte Standort[]
|
||||
hosts Host[]
|
||||
kundenrechnungen Kundenrechnung[]
|
||||
enrollmentTokens EnrollmentToken[]
|
||||
alerts Alert[]
|
||||
|
||||
@@index([name])
|
||||
}
|
||||
|
||||
model Standort {
|
||||
id String @id @default(cuid())
|
||||
kundeId String
|
||||
name String
|
||||
strasse String?
|
||||
plz String?
|
||||
ort String?
|
||||
notizen String?
|
||||
erstelltAm DateTime @default(now())
|
||||
geaendertAm DateTime @updatedAt
|
||||
|
||||
kunde Kunde @relation(fields: [kundeId], references: [id], onDelete: Cascade)
|
||||
hosts Host[]
|
||||
enrollmentTokens EnrollmentToken[]
|
||||
|
||||
@@index([kundeId])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Hosts
|
||||
|
||||
model Host {
|
||||
id String @id @default(cuid())
|
||||
|
||||
/// Stabile Kennung, die der Agent bei der Registrierung erzeugt (aus /etc/machine-id abgeleitet).
|
||||
/// Sie bleibt auch dann gleich, wenn sich die Proxmox-Server-ID durch Hardwaretausch ändert.
|
||||
agentId String @unique
|
||||
|
||||
hostname String
|
||||
fqdn String?
|
||||
kundeId String?
|
||||
standortId String?
|
||||
produkt Produkt
|
||||
produktVersion String?
|
||||
kernel String?
|
||||
|
||||
/// Aktuelle Server-ID laut letztem Report; die Historie steht in ServerIdHistorie.
|
||||
serverId String?
|
||||
clusterName String?
|
||||
nodeName String?
|
||||
ips String[] @default([])
|
||||
sockets Int?
|
||||
cpuModell String?
|
||||
uptimeSekunden BigInt?
|
||||
paketUpdates Int?
|
||||
agentVersion String?
|
||||
notizen String?
|
||||
|
||||
// Momentaufnahme der Subscription aus dem letzten Report (für Listen und Filter).
|
||||
subKey String?
|
||||
subStatus SubscriptionStatus @default(UNKNOWN)
|
||||
subLevel String?
|
||||
subProduktname String?
|
||||
subLizenzierteSockets Int?
|
||||
subRegdate DateTime? @db.Date
|
||||
subNextduedate DateTime? @db.Date
|
||||
subChecktime DateTime?
|
||||
|
||||
erstmalsGesehenAm DateTime @default(now())
|
||||
letzterReportAm DateTime?
|
||||
erstelltAm DateTime @default(now())
|
||||
geaendertAm DateTime @updatedAt
|
||||
|
||||
kunde Kunde? @relation(fields: [kundeId], references: [id], onDelete: SetNull)
|
||||
standort Standort? @relation(fields: [standortId], references: [id], onDelete: SetNull)
|
||||
reports Report[]
|
||||
keyHistorie SubscriptionKeyHistorie[]
|
||||
serverIdHistorie ServerIdHistorie[]
|
||||
laufzeiten Laufzeit[]
|
||||
tokens AgentToken[]
|
||||
alerts Alert[]
|
||||
tags Tag[] @relation("HostTags")
|
||||
|
||||
@@index([kundeId])
|
||||
@@index([standortId])
|
||||
@@index([hostname])
|
||||
@@index([serverId])
|
||||
@@index([subKey])
|
||||
@@index([subNextduedate])
|
||||
@@index([subStatus])
|
||||
}
|
||||
|
||||
model Tag {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
farbe String?
|
||||
|
||||
hosts Host[] @relation("HostTags")
|
||||
}
|
||||
|
||||
model Report {
|
||||
id String @id @default(cuid())
|
||||
hostId String
|
||||
|
||||
/// Vom Agent erzeugte UUID; verhindert doppelte Verarbeitung nachgesendeter Reports.
|
||||
reportId String @unique
|
||||
|
||||
/// Zeitpunkt der Datenerhebung auf dem Host (kann bei nachgesendeten Reports älter sein).
|
||||
erhobenAm DateTime
|
||||
empfangenAm DateTime @default(now())
|
||||
|
||||
subStatus SubscriptionStatus
|
||||
subKey String?
|
||||
subNextduedate DateTime? @db.Date
|
||||
serverId String?
|
||||
hostname String?
|
||||
produktVersion String?
|
||||
kernel String?
|
||||
sockets Int?
|
||||
paketUpdates Int?
|
||||
agentVersion String?
|
||||
ips String[] @default([])
|
||||
quellIp String?
|
||||
|
||||
/// Vollständiger Report, damit auch künftige Felder nachträglich auswertbar sind.
|
||||
rohdaten Json
|
||||
|
||||
host Host @relation(fields: [hostId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([hostId, erhobenAm])
|
||||
@@index([empfangenAm])
|
||||
}
|
||||
|
||||
model SubscriptionKeyHistorie {
|
||||
id String @id @default(cuid())
|
||||
hostId String
|
||||
key String
|
||||
ersteMeldung DateTime @default(now())
|
||||
letzteMeldung DateTime @default(now())
|
||||
|
||||
host Host @relation(fields: [hostId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([hostId, key])
|
||||
@@index([key])
|
||||
}
|
||||
|
||||
model ServerIdHistorie {
|
||||
id String @id @default(cuid())
|
||||
hostId String
|
||||
serverId String
|
||||
ersteMeldung DateTime @default(now())
|
||||
letzteMeldung DateTime @default(now())
|
||||
|
||||
host Host @relation(fields: [hostId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([hostId, serverId])
|
||||
@@index([serverId])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Laufzeiten und Rechnungen
|
||||
|
||||
model Laufzeit {
|
||||
id String @id @default(cuid())
|
||||
hostId String
|
||||
key String
|
||||
start DateTime @db.Date
|
||||
ende DateTime @db.Date
|
||||
status LaufzeitStatus @default(VORSCHLAG)
|
||||
notizen String?
|
||||
|
||||
/// Position der Proxmox-Rechnung (Einkauf), die diese Laufzeit bezahlt.
|
||||
proxmoxPositionId String? @unique
|
||||
|
||||
/// Position der Kundenrechnung (Verkauf), mit der die Laufzeit weiterberechnet wird.
|
||||
kundenPositionId String? @unique
|
||||
|
||||
erstelltAm DateTime @default(now())
|
||||
geaendertAm DateTime @updatedAt
|
||||
|
||||
host Host @relation(fields: [hostId], references: [id], onDelete: Cascade)
|
||||
proxmoxPosition ProxmoxRechnungPosition? @relation(fields: [proxmoxPositionId], references: [id], onDelete: SetNull)
|
||||
kundenPosition KundenrechnungPosition? @relation(fields: [kundenPositionId], references: [id], onDelete: SetNull)
|
||||
alerts Alert[]
|
||||
|
||||
@@unique([hostId, start])
|
||||
@@index([ende])
|
||||
@@index([key])
|
||||
}
|
||||
|
||||
/// Rechnung von Proxmox (Einkauf). Eine Bestellung kann mehrere Keys umfassen.
|
||||
model ProxmoxRechnung {
|
||||
id String @id @default(cuid())
|
||||
nummer String @unique
|
||||
datum DateTime @db.Date
|
||||
faelligAm DateTime? @db.Date
|
||||
nettoBetrag Decimal @db.Decimal(12, 2)
|
||||
ustSatz Decimal @default(19.00) @db.Decimal(5, 2)
|
||||
rabatt Decimal? @db.Decimal(12, 2)
|
||||
zahlungsart String?
|
||||
bezahlt Boolean @default(false)
|
||||
bezahltAm DateTime? @db.Date
|
||||
notizen String?
|
||||
anhangId String?
|
||||
erstelltAm DateTime @default(now())
|
||||
geaendertAm DateTime @updatedAt
|
||||
|
||||
anhang Anhang? @relation(fields: [anhangId], references: [id], onDelete: SetNull)
|
||||
positionen ProxmoxRechnungPosition[]
|
||||
|
||||
@@index([bezahlt, faelligAm])
|
||||
@@index([datum])
|
||||
}
|
||||
|
||||
model ProxmoxRechnungPosition {
|
||||
id String @id @default(cuid())
|
||||
rechnungId String
|
||||
bezeichnung String
|
||||
menge Int @default(1)
|
||||
nettoBetrag Decimal @db.Decimal(12, 2)
|
||||
|
||||
rechnung ProxmoxRechnung @relation(fields: [rechnungId], references: [id], onDelete: Cascade)
|
||||
laufzeit Laufzeit?
|
||||
|
||||
@@index([rechnungId])
|
||||
}
|
||||
|
||||
/// Rechnung an den Kunden (Verkauf). `erstellt = false` bedeutet: steht noch aus.
|
||||
model Kundenrechnung {
|
||||
id String @id @default(cuid())
|
||||
kundeId String
|
||||
erstellt Boolean @default(false)
|
||||
nummer String? @unique
|
||||
datum DateTime? @db.Date
|
||||
nettoBetrag Decimal? @db.Decimal(12, 2)
|
||||
ustSatz Decimal @default(19.00) @db.Decimal(5, 2)
|
||||
bezahlt Boolean @default(false)
|
||||
bezahltAm DateTime? @db.Date
|
||||
notizen String?
|
||||
anhangId String?
|
||||
erstelltAm DateTime @default(now())
|
||||
geaendertAm DateTime @updatedAt
|
||||
|
||||
kunde Kunde @relation(fields: [kundeId], references: [id], onDelete: Cascade)
|
||||
anhang Anhang? @relation(fields: [anhangId], references: [id], onDelete: SetNull)
|
||||
positionen KundenrechnungPosition[]
|
||||
|
||||
@@index([kundeId])
|
||||
@@index([erstellt, bezahlt])
|
||||
}
|
||||
|
||||
model KundenrechnungPosition {
|
||||
id String @id @default(cuid())
|
||||
rechnungId String
|
||||
bezeichnung String
|
||||
menge Int @default(1)
|
||||
nettoBetrag Decimal @db.Decimal(12, 2)
|
||||
|
||||
rechnung Kundenrechnung @relation(fields: [rechnungId], references: [id], onDelete: Cascade)
|
||||
laufzeit Laufzeit?
|
||||
|
||||
@@index([rechnungId])
|
||||
}
|
||||
|
||||
model Anhang {
|
||||
id String @id @default(cuid())
|
||||
dateiname String
|
||||
pfad String
|
||||
mimeTyp String
|
||||
groesse Int
|
||||
sha256 String
|
||||
hochgeladenAm DateTime @default(now())
|
||||
|
||||
proxmoxRechnungen ProxmoxRechnung[]
|
||||
kundenrechnungen Kundenrechnung[]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Tokens
|
||||
|
||||
/// Host-spezifischer API-Token. In der Datenbank liegt nur der SHA-256-Hash.
|
||||
model AgentToken {
|
||||
id String @id @default(cuid())
|
||||
hostId String
|
||||
tokenHash String @unique
|
||||
praefix String
|
||||
bezeichnung String?
|
||||
erstelltAm DateTime @default(now())
|
||||
letzteNutzungAm DateTime?
|
||||
widerrufenAm DateTime?
|
||||
|
||||
host Host @relation(fields: [hostId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([hostId])
|
||||
}
|
||||
|
||||
/// Token für die Erstregistrierung, optional mit Kunden- und Standortzuordnung.
|
||||
model EnrollmentToken {
|
||||
id String @id @default(cuid())
|
||||
tokenHash String @unique
|
||||
praefix String
|
||||
bezeichnung String?
|
||||
gueltigBis DateTime?
|
||||
maxNutzungen Int?
|
||||
nutzungen Int @default(0)
|
||||
kundeId String?
|
||||
standortId String?
|
||||
erstelltAm DateTime @default(now())
|
||||
widerrufenAm DateTime?
|
||||
|
||||
kunde Kunde? @relation(fields: [kundeId], references: [id], onDelete: SetNull)
|
||||
standort Standort? @relation(fields: [standortId], references: [id], onDelete: SetNull)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Warnungen und Benachrichtigungen
|
||||
|
||||
model Alert {
|
||||
id String @id @default(cuid())
|
||||
regel String
|
||||
|
||||
/// Regel + betroffenes Objekt (+ Schwelle). Verhindert doppelte offene Warnungen.
|
||||
fingerprint String @unique
|
||||
|
||||
schwere AlertSchwere
|
||||
titel String
|
||||
beschreibung String
|
||||
|
||||
hostId String?
|
||||
kundeId String?
|
||||
laufzeitId String?
|
||||
|
||||
geoeffnetAm DateTime @default(now())
|
||||
quittiertAm DateTime?
|
||||
erledigtAm DateTime?
|
||||
|
||||
host Host? @relation(fields: [hostId], references: [id], onDelete: Cascade)
|
||||
kunde Kunde? @relation(fields: [kundeId], references: [id], onDelete: Cascade)
|
||||
laufzeit Laufzeit? @relation(fields: [laufzeitId], references: [id], onDelete: Cascade)
|
||||
benachrichtigungen Benachrichtigung[]
|
||||
|
||||
@@index([erledigtAm])
|
||||
@@index([hostId])
|
||||
@@index([kundeId])
|
||||
@@index([regel])
|
||||
}
|
||||
|
||||
model Benachrichtigung {
|
||||
id String @id @default(cuid())
|
||||
alertId String?
|
||||
kanal NotificationKanal
|
||||
empfaenger String
|
||||
betreff String?
|
||||
status NotificationStatus @default(WARTEND)
|
||||
fehler String?
|
||||
versendetAm DateTime?
|
||||
erstelltAm DateTime @default(now())
|
||||
|
||||
alert Alert? @relation(fields: [alertId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([alertId])
|
||||
@@index([erstelltAm])
|
||||
}
|
||||
|
||||
/// Schwellwerte je Warnregel, z. B. { "tage": [30, 14, 7] }.
|
||||
model WarnregelEinstellung {
|
||||
id String @id @default(cuid())
|
||||
regel String @unique
|
||||
aktiv Boolean @default(true)
|
||||
schwellwerte Json @default("{}")
|
||||
geaendertAm DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Benachrichtigungskanal {
|
||||
id String @id @default(cuid())
|
||||
typ NotificationKanal
|
||||
name String
|
||||
aktiv Boolean @default(true)
|
||||
konfiguration Json
|
||||
erstelltAm DateTime @default(now())
|
||||
geaendertAm DateTime @updatedAt
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Benutzer und Protokoll
|
||||
|
||||
/// Alle Benutzer sind Administratoren; eine Nur-Lese-Rolle ist bewusst nicht vorgesehen.
|
||||
model Benutzer {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
name String
|
||||
passwortHash String?
|
||||
oidcSubject String? @unique
|
||||
aktiv Boolean @default(true)
|
||||
letzterLoginAm DateTime?
|
||||
erstelltAm DateTime @default(now())
|
||||
geaendertAm DateTime @updatedAt
|
||||
|
||||
auditEintraege AuditEintrag[]
|
||||
}
|
||||
|
||||
model AuditEintrag {
|
||||
id String @id @default(cuid())
|
||||
benutzerId String?
|
||||
aktion String
|
||||
entitaet String
|
||||
entitaetId String?
|
||||
aenderung Json?
|
||||
ipAdresse String?
|
||||
erstelltAm DateTime @default(now())
|
||||
|
||||
benutzer Benutzer? @relation(fields: [benutzerId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([entitaet, entitaetId])
|
||||
@@index([erstelltAm])
|
||||
}
|
||||
|
||||
/// Freie Systemeinstellungen (z. B. SMTP-Konfiguration, Anzeigeoptionen).
|
||||
model Einstellung {
|
||||
schluessel String @id
|
||||
wert Json
|
||||
geaendertAm DateTime @updatedAt
|
||||
}
|
||||
+296
@@ -0,0 +1,296 @@
|
||||
import { hash } from "@node-rs/argon2";
|
||||
import { PrismaPg } from "@prisma/adapter-pg";
|
||||
import { PrismaClient } from "../src/generated/prisma/client";
|
||||
import type { Prisma } from "../src/generated/prisma/client";
|
||||
import type { Produkt, SubscriptionStatus } from "../src/generated/prisma/enums";
|
||||
import { alerts, hosts, kunden, laufzeiten, reports } from "../src/mock/data";
|
||||
import type { Laufzeit as MockLaufzeit } from "../src/mock/types";
|
||||
|
||||
const connectionString = process.env.DATABASE_URL;
|
||||
if (!connectionString) throw new Error("DATABASE_URL ist nicht gesetzt.");
|
||||
|
||||
const prisma = new PrismaClient({ adapter: new PrismaPg({ connectionString }) });
|
||||
|
||||
/** Standardwerte der Warnregeln; Schwellwerte sind später in der Oberfläche änderbar (#011). */
|
||||
const WARNREGELN: { regel: string; schwellwerte: Prisma.InputJsonValue }[] = [
|
||||
{ regel: "subscription-laeuft-ab", schwellwerte: { tage: [30, 14, 7] } },
|
||||
{ regel: "status-nicht-aktiv", schwellwerte: {} },
|
||||
{ regel: "sockets-abweichung", schwellwerte: {} },
|
||||
{ regel: "keine-meldung", schwellwerte: { stunden: 48 } },
|
||||
{ regel: "serverid-geaendert", schwellwerte: {} },
|
||||
{ regel: "key-mehrfach-gemeldet", schwellwerte: {} },
|
||||
{ regel: "host-ohne-kunde", schwellwerte: {} },
|
||||
{ regel: "proxmox-rechnung-faellig", schwellwerte: { tage: 7 } },
|
||||
{ regel: "kundenrechnung-fehlt", schwellwerte: {} },
|
||||
{ regel: "kundenrechnung-unbezahlt", schwellwerte: { tage: 30 } },
|
||||
];
|
||||
|
||||
async function seedAdmin() {
|
||||
const email = process.env.ADMIN_EMAIL ?? "admin@example.de";
|
||||
const name = process.env.ADMIN_NAME ?? "Administrator";
|
||||
const passwort = process.env.ADMIN_PASSWORD;
|
||||
|
||||
if (!passwort) {
|
||||
console.warn("ADMIN_PASSWORD ist nicht gesetzt – der Administrator wird ohne Passwort angelegt.");
|
||||
}
|
||||
|
||||
const passwortHash = passwort ? await hash(passwort) : null;
|
||||
|
||||
await prisma.benutzer.upsert({
|
||||
where: { email },
|
||||
// Ein vorhandenes Passwort wird nicht überschrieben, damit der Seed gefahrlos wiederholbar ist.
|
||||
update: { name },
|
||||
create: { email, name, passwortHash },
|
||||
});
|
||||
|
||||
console.log(`Administrator: ${email}`);
|
||||
}
|
||||
|
||||
async function seedWarnregeln() {
|
||||
for (const regel of WARNREGELN) {
|
||||
await prisma.warnregelEinstellung.upsert({
|
||||
where: { regel: regel.regel },
|
||||
update: {},
|
||||
create: { regel: regel.regel, aktiv: true, schwellwerte: regel.schwellwerte },
|
||||
});
|
||||
}
|
||||
console.log(`${WARNREGELN.length} Warnregeln vorbereitet`);
|
||||
}
|
||||
|
||||
const PRODUKT: Record<string, Produkt> = { pve: "PVE", pbs: "PBS", pmg: "PMG" };
|
||||
const STATUS: Record<string, SubscriptionStatus> = {
|
||||
active: "ACTIVE",
|
||||
notfound: "NOTFOUND",
|
||||
invalid: "INVALID",
|
||||
expired: "EXPIRED",
|
||||
suspended: "SUSPENDED",
|
||||
};
|
||||
|
||||
function datum(wert: string | null | undefined): Date | null {
|
||||
return wert ? new Date(wert) : null;
|
||||
}
|
||||
|
||||
async function seedDemo() {
|
||||
for (const kunde of kunden) {
|
||||
await prisma.kunde.upsert({
|
||||
where: { id: kunde.id },
|
||||
update: {},
|
||||
create: {
|
||||
id: kunde.id,
|
||||
name: kunde.name,
|
||||
kundennummer: kunde.kundennummer,
|
||||
ansprechpartner: kunde.ansprechpartner,
|
||||
email: kunde.email,
|
||||
telefon: kunde.telefon,
|
||||
notizen: kunde.notizen,
|
||||
standorte: {
|
||||
create: kunde.standorte.map((standort) => ({
|
||||
id: standort.id,
|
||||
name: standort.name,
|
||||
ort: standort.ort,
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (const host of hosts) {
|
||||
await prisma.host.upsert({
|
||||
where: { id: host.id },
|
||||
update: {},
|
||||
create: {
|
||||
id: host.id,
|
||||
agentId: `demo-${host.id}`,
|
||||
hostname: host.hostname,
|
||||
fqdn: host.fqdn,
|
||||
kundeId: host.kundeId,
|
||||
standortId: host.standortId,
|
||||
produkt: PRODUKT[host.produkt],
|
||||
produktVersion: host.produktVersion,
|
||||
kernel: host.kernel,
|
||||
serverId: host.serverId,
|
||||
clusterName: host.clusterName,
|
||||
nodeName: host.nodeName,
|
||||
ips: host.ips,
|
||||
sockets: host.sockets,
|
||||
cpuModell: host.cpuModell,
|
||||
uptimeSekunden: host.uptimeSekunden === null ? null : BigInt(host.uptimeSekunden),
|
||||
paketUpdates: host.paketUpdates,
|
||||
agentVersion: host.agentVersion,
|
||||
notizen: host.notizen,
|
||||
subKey: host.subscription.key,
|
||||
subStatus: STATUS[host.subscription.status],
|
||||
subLevel: host.subscription.level,
|
||||
subProduktname: host.subscription.produktname,
|
||||
subLizenzierteSockets: host.subscription.lizenzierteSockets,
|
||||
subRegdate: datum(host.subscription.regdate),
|
||||
subNextduedate: datum(host.subscription.nextduedate),
|
||||
subChecktime: datum(host.subscription.checktime),
|
||||
letzterReportAm: datum(host.letzterReport),
|
||||
tags: {
|
||||
connectOrCreate: host.tags.map((tag) => ({ where: { name: tag }, create: { name: tag } })),
|
||||
},
|
||||
keyHistorie: host.subscription.key
|
||||
? { create: [{ key: host.subscription.key }] }
|
||||
: undefined,
|
||||
serverIdHistorie: { create: [{ serverId: host.serverId }] },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Rechnungen entstehen aus den Laufzeiten: gleiche Rechnungsnummer = eine Rechnung mit mehreren Positionen.
|
||||
const proxmoxRechnungen = new Map<string, MockLaufzeit[]>();
|
||||
const kundenrechnungen = new Map<string, MockLaufzeit[]>();
|
||||
|
||||
for (const laufzeit of laufzeiten) {
|
||||
if (laufzeit.einkauf?.nummer) {
|
||||
const bisher = proxmoxRechnungen.get(laufzeit.einkauf.nummer) ?? [];
|
||||
proxmoxRechnungen.set(laufzeit.einkauf.nummer, [...bisher, laufzeit]);
|
||||
}
|
||||
if (laufzeit.verkauf) {
|
||||
const schluessel = laufzeit.verkauf.nummer ?? `ohne-nummer-${laufzeit.id}`;
|
||||
const bisher = kundenrechnungen.get(schluessel) ?? [];
|
||||
kundenrechnungen.set(schluessel, [...bisher, laufzeit]);
|
||||
}
|
||||
}
|
||||
|
||||
const proxmoxPositionen = new Map<string, string>();
|
||||
for (const [nummer, zugehoerig] of proxmoxRechnungen) {
|
||||
const vorlage = zugehoerig[0].einkauf!;
|
||||
const rechnung = await prisma.proxmoxRechnung.upsert({
|
||||
where: { nummer },
|
||||
update: {},
|
||||
create: {
|
||||
nummer,
|
||||
datum: new Date(vorlage.datum!),
|
||||
faelligAm: datum(vorlage.faelligAm),
|
||||
nettoBetrag: zugehoerig.reduce((summe, l) => summe + (l.einkauf?.nettoBetrag ?? 0), 0),
|
||||
ustSatz: vorlage.ustSatz,
|
||||
rabatt: vorlage.rabatt ?? null,
|
||||
zahlungsart: vorlage.zahlungsart ?? null,
|
||||
bezahlt: vorlage.bezahlt,
|
||||
bezahltAm: datum(vorlage.bezahltAm),
|
||||
positionen: {
|
||||
create: zugehoerig.map((l) => ({
|
||||
id: `prp-${l.id}`,
|
||||
bezeichnung: `${l.key} · ${l.start} bis ${l.ende}`,
|
||||
nettoBetrag: l.einkauf?.nettoBetrag ?? 0,
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
for (const l of zugehoerig) proxmoxPositionen.set(l.id, `prp-${l.id}`);
|
||||
console.log(`Proxmox-Rechnung ${rechnung.nummer} mit ${zugehoerig.length} Position(en)`);
|
||||
}
|
||||
|
||||
const kundenPositionen = new Map<string, string>();
|
||||
for (const [schluessel, zugehoerig] of kundenrechnungen) {
|
||||
const vorlage = zugehoerig[0].verkauf!;
|
||||
const host = hosts.find((h) => h.id === zugehoerig[0].hostId);
|
||||
if (!host?.kundeId) continue;
|
||||
|
||||
const id = `kr-${schluessel}`;
|
||||
await prisma.kundenrechnung.upsert({
|
||||
where: { id },
|
||||
update: {},
|
||||
create: {
|
||||
id,
|
||||
kundeId: host.kundeId,
|
||||
erstellt: vorlage.erstellt,
|
||||
nummer: vorlage.nummer,
|
||||
datum: datum(vorlage.datum),
|
||||
nettoBetrag: vorlage.erstellt
|
||||
? zugehoerig.reduce((summe, l) => summe + (l.verkauf?.nettoBetrag ?? 0), 0)
|
||||
: null,
|
||||
ustSatz: vorlage.ustSatz,
|
||||
bezahlt: vorlage.bezahlt,
|
||||
bezahltAm: datum(vorlage.bezahltAm),
|
||||
positionen: {
|
||||
create: zugehoerig.map((l) => ({
|
||||
id: `krp-${l.id}`,
|
||||
bezeichnung: `${l.key} · ${l.start} bis ${l.ende}`,
|
||||
nettoBetrag: l.verkauf?.nettoBetrag ?? 0,
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
for (const l of zugehoerig) kundenPositionen.set(l.id, `krp-${l.id}`);
|
||||
}
|
||||
|
||||
for (const laufzeit of laufzeiten) {
|
||||
await prisma.laufzeit.upsert({
|
||||
where: { id: laufzeit.id },
|
||||
update: {},
|
||||
create: {
|
||||
id: laufzeit.id,
|
||||
hostId: laufzeit.hostId,
|
||||
key: laufzeit.key,
|
||||
start: new Date(laufzeit.start),
|
||||
ende: new Date(laufzeit.ende),
|
||||
status: laufzeit.vorschlag ? "VORSCHLAG" : "BESTAETIGT",
|
||||
proxmoxPositionId: proxmoxPositionen.get(laufzeit.id) ?? null,
|
||||
kundenPositionId: kundenPositionen.get(laufzeit.id) ?? null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (const report of reports) {
|
||||
await prisma.report.upsert({
|
||||
where: { reportId: report.id },
|
||||
update: {},
|
||||
create: {
|
||||
reportId: report.id,
|
||||
hostId: report.hostId,
|
||||
erhobenAm: new Date(report.zeitpunkt),
|
||||
subStatus: STATUS[report.status],
|
||||
subKey: report.key,
|
||||
subNextduedate: datum(report.nextduedate),
|
||||
agentVersion: report.agentVersion,
|
||||
paketUpdates: report.paketUpdates,
|
||||
rohdaten: { hinweis: "Demo-Report ohne Rohdaten" },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (const alert of alerts) {
|
||||
await prisma.alert.upsert({
|
||||
where: { fingerprint: `${alert.typ}:${alert.hostId ?? alert.kundeId ?? alert.id}` },
|
||||
update: {},
|
||||
create: {
|
||||
id: alert.id,
|
||||
fingerprint: `${alert.typ}:${alert.hostId ?? alert.kundeId ?? alert.id}`,
|
||||
regel: alert.typ,
|
||||
schwere: alert.schwere === "gefahr" ? "GEFAHR" : alert.schwere === "warnung" ? "WARNUNG" : "INFO",
|
||||
titel: alert.titel,
|
||||
beschreibung: alert.beschreibung,
|
||||
hostId: alert.hostId,
|
||||
kundeId: alert.kundeId,
|
||||
laufzeitId: alert.laufzeitId,
|
||||
geoeffnetAm: new Date(alert.seit),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Demo-Daten: ${kunden.length} Kunden, ${hosts.length} Hosts, ${laufzeiten.length} Laufzeiten, ${alerts.length} Warnungen`,
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await seedAdmin();
|
||||
await seedWarnregeln();
|
||||
|
||||
if (process.env.SEED_DEMO === "1") {
|
||||
await seedDemo();
|
||||
} else {
|
||||
console.log("Demo-Daten übersprungen (SEED_DEMO=1 setzen, um sie anzulegen).");
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => prisma.$disconnect())
|
||||
.catch(async (fehler) => {
|
||||
console.error(fehler);
|
||||
await prisma.$disconnect();
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user