Basis: Certvia dev@a48c5fb als Fundament für Craftvia
CI / build-and-check (push) Canceled after 0s
CI / audit (push) Canceled after 0s
CI / sbom (push) Canceled after 0s

Unveränderter Stand von certvia/dev (a48c5fb) plus Craftvia-Spezifikation
und Brandbook unter docs/craftvia/. ISMS-Module werden im Folgecommit entfernt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 11:05:39 +02:00
co-authored by Claude Opus 5
commit c8e6f30a27
720 changed files with 140143 additions and 0 deletions
@@ -0,0 +1,54 @@
-- ============================================================================
-- WS0 (Option C) — Fundament: globale `Identity` + `User.identity_id`
-- ============================================================================
-- Umbau "Zentrale Identität mit Mandanten-Mitgliedschaften" (docs/FEINDESIGN-
-- identity-mandanten.md). Dieser Schritt ist ADDITIV (Expand/Contract):
-- * `identities` wird neu angelegt und ist GLOBAL — bewusst OHNE
-- `tenant_id`, OHNE RLS/Policy und NICHT in TENANT_MODELS. Der Zugriff läuft
-- ausschließlich über den Owner-`prisma`-Client (wie heute der Login-Lookup).
-- isms_app (RLS-Rolle) greift nie direkt auf `identities` zu; die FK-Prüfung
-- beim INSERT in `users` läuft über den internen RI-Trigger (Owner-Recht) und
-- braucht kein SELECT-GRANT für isms_app.
-- * `users` behält in diesem Schritt seine Auth-Spalten (Legacy), damit Login/
-- Guards grün und lauffähig bleiben. Die Konsumenten wandern in WS1–WS4 auf
-- Identity; die dann ungenutzten Spalten entfernt eine spätere Contract-Migration.
--
-- Hinweis Entscheidung D: kein Backfill/Merge. `identity_id` ist NOT NULL — die
-- Migration wird deshalb auf einer LEEREN `users`-Tabelle angewandt (Test/Coolify:
-- DB leeren -> migrate deploy -> seed; lokal via `prisma migrate reset --force`).
-- ============================================================================
-- CreateTable (GLOBAL — keine RLS!)
CREATE TABLE "identities" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password_hash" TEXT NOT NULL,
"must_change_password" BOOLEAN NOT NULL DEFAULT false,
"mfa_secret" TEXT,
"mfa_enrolled_at" TIMESTAMP(3),
"recovery_codes" JSONB NOT NULL DEFAULT '[]',
"last_totp_step" BIGINT,
"failed_logins" INTEGER NOT NULL DEFAULT 0,
"locked_until" TIMESTAMP(3),
"sessions_valid_after" TIMESTAMP(3),
"status" TEXT NOT NULL DEFAULT 'ACTIVE',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "identities_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "identities_email_key" ON "identities"("email");
-- AlterTable: Mitgliedschaft -> Identity (Option C)
ALTER TABLE "users" ADD COLUMN "identity_id" TEXT NOT NULL;
-- CreateIndex
CREATE INDEX "users_identity_id_idx" ON "users"("identity_id");
-- CreateIndex: eine Person max. 1 Mitgliedschaft je Mandant
CREATE UNIQUE INDEX "users_tenant_id_identity_id_key" ON "users"("tenant_id", "identity_id");
-- AddForeignKey
ALTER TABLE "users" ADD CONSTRAINT "users_identity_id_fkey" FOREIGN KEY ("identity_id") REFERENCES "identities"("id") ON DELETE RESTRICT ON UPDATE CASCADE;