Next.js 16, Prisma 7, Tailwind 4 und Vitest/Playwright, Konfiguration aus Laufzettel übernommen, weil dort derselbe Stack erprobt ist. Die Farben der Branding-Vorgabe stehen als Tokens in globals.css, das helle Theme setzt dieselben Namen neu. Amber und Rot sind dort abgedunkelt, sonst reicht der Kontrast nicht – ein Test prüft beide Themes gegen 4,5:1. Anmeldung mit lokalem Benutzer, Rolle je Anfrage aus der Datenbank, Audit-Log per Trigger nur anhängbar. Geprüft im gebauten Stand auf 1440 px und 390 px. Closes #001 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
86 lines
2.7 KiB
PL/PgSQL
86 lines
2.7 KiB
PL/PgSQL
-- CreateEnum
|
||
CREATE TYPE "Rolle" AS ENUM ('ADMIN', 'OPERATOR');
|
||
|
||
-- CreateEnum
|
||
CREATE TYPE "Quelle" AS ENUM ('WEB', 'CLI', 'API', 'SYSTEM');
|
||
|
||
-- CreateTable
|
||
CREATE TABLE "Benutzer" (
|
||
"id" TEXT NOT NULL,
|
||
"benutzername" TEXT NOT NULL,
|
||
"anzeigename" TEXT NOT NULL,
|
||
"email" TEXT,
|
||
"passwortHash" TEXT,
|
||
"oidcSubjekt" TEXT,
|
||
"rolle" "Rolle" NOT NULL DEFAULT 'OPERATOR',
|
||
"aktiv" BOOLEAN NOT NULL DEFAULT true,
|
||
"standards" JSONB,
|
||
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
"geaendertAm" TIMESTAMP(3) NOT NULL,
|
||
|
||
CONSTRAINT "Benutzer_pkey" PRIMARY KEY ("id")
|
||
);
|
||
|
||
-- CreateTable
|
||
CREATE TABLE "Protokoll" (
|
||
"id" BIGSERIAL NOT NULL,
|
||
"zeit" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
"benutzerId" TEXT,
|
||
"quelle" "Quelle" NOT NULL,
|
||
"aktion" TEXT NOT NULL,
|
||
"ziel" TEXT,
|
||
"details" JSONB,
|
||
"ip" TEXT,
|
||
|
||
CONSTRAINT "Protokoll_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")
|
||
);
|
||
|
||
-- CreateIndex
|
||
CREATE UNIQUE INDEX "Benutzer_benutzername_key" ON "Benutzer"("benutzername");
|
||
|
||
-- CreateIndex
|
||
CREATE UNIQUE INDEX "Benutzer_oidcSubjekt_key" ON "Benutzer"("oidcSubjekt");
|
||
|
||
-- CreateIndex
|
||
CREATE INDEX "Protokoll_zeit_idx" ON "Protokoll"("zeit");
|
||
|
||
-- CreateIndex
|
||
CREATE INDEX "Protokoll_benutzerId_zeit_idx" ON "Protokoll"("benutzerId", "zeit");
|
||
|
||
-- AddForeignKey
|
||
ALTER TABLE "Protokoll" ADD CONSTRAINT "Protokoll_benutzerId_fkey" FOREIGN KEY ("benutzerId") REFERENCES "Benutzer"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||
|
||
-- Regeln, die auch gelten, wenn jemand an der Service-Schicht vorbei schreibt.
|
||
|
||
-- Das Audit-Log ist nur anhängbar. Eine Ausnahme braucht es nicht: auch das Löschen
|
||
-- eines Benutzers setzt benutzerId per FK auf NULL, und das ist ein UPDATE – das
|
||
-- erlaubt der Trigger nur für genau diese Spalte.
|
||
CREATE FUNCTION "protokoll_nur_anhaengen"() RETURNS trigger AS $$
|
||
BEGIN
|
||
IF TG_OP = 'UPDATE'
|
||
AND NEW."benutzerId" IS NULL
|
||
AND (NEW.id, NEW.zeit, NEW.quelle, NEW.aktion, NEW.ziel, NEW.details, NEW.ip)
|
||
IS NOT DISTINCT FROM (OLD.id, OLD.zeit, OLD.quelle, OLD.aktion, OLD.ziel, OLD.details, OLD.ip) THEN
|
||
RETURN NEW;
|
||
END IF;
|
||
RAISE EXCEPTION 'Das Protokoll ist nur anhängbar (%).', TG_OP USING ERRCODE = 'P0001';
|
||
END;
|
||
$$ LANGUAGE plpgsql;
|
||
|
||
CREATE TRIGGER "protokoll_nur_anhaengen"
|
||
BEFORE UPDATE OR DELETE ON "Protokoll"
|
||
FOR EACH ROW EXECUTE FUNCTION "protokoll_nur_anhaengen"();
|
||
|
||
ALTER TABLE "Benutzer"
|
||
ADD CONSTRAINT "benutzer_benutzername_format"
|
||
CHECK ("benutzername" ~ '^[a-z0-9][a-z0-9._-]{1,62}$');
|