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>
55 lines
2.7 KiB
SQL
55 lines
2.7 KiB
SQL
-- ============================================================================
|
||
-- 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;
|