feat: Grundgerüst mit Anmeldung, Layout und Design-Tokens

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>
This commit is contained in:
2026-10-12 09:23:41 +02:00
co-authored by Claude Opus 5.5
parent c3211d0fe2
commit 871bb3a77c
75 changed files with 10155 additions and 15 deletions
@@ -0,0 +1,85 @@
-- 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}$');