-- ============================================================================ -- 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;