-- DropForeignKey ALTER TABLE "audit_logs" DROP CONSTRAINT "audit_logs_tenant_id_fkey"; -- AlterTable ALTER TABLE "audit_logs" ALTER COLUMN "tenant_id" DROP NOT NULL; -- CreateTable CREATE TABLE "platform_admins" ( "id" TEXT NOT NULL, "email" TEXT NOT NULL, "password_hash" TEXT NOT NULL, "name" TEXT NOT NULL, "status" TEXT NOT NULL DEFAULT 'ACTIVE', "mfa_secret" TEXT, "mfa_enrolled_at" TIMESTAMP(3), "recovery_codes" JSONB NOT NULL DEFAULT '[]', "failed_logins" INTEGER NOT NULL DEFAULT 0, "locked_until" TIMESTAMP(3), "last_login_at" TIMESTAMP(3), "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updated_at" TIMESTAMP(3) NOT NULL, CONSTRAINT "platform_admins_pkey" PRIMARY KEY ("id") ); -- CreateIndex CREATE UNIQUE INDEX "platform_admins_email_key" ON "platform_admins"("email"); -- AddForeignKey ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- Datenmigration (Phase-1-Härtung Paket 2): bestehende isPlatformAdmin-Nutzer in den -- getrennten PlatformAdmin-Store übernehmen (gleicher Argon2-Hash → Login sofort möglich) -- und das Mandanten-Flag zurücksetzen. MFA wird beim ersten Plattform-Login eingerichtet. INSERT INTO "platform_admins" ("id", "email", "password_hash", "name", "status", "recovery_codes", "failed_logins", "created_at", "updated_at") SELECT gen_random_uuid()::text, lower("email"), "password_hash", "name", 'ACTIVE', '[]'::jsonb, 0, now(), now() FROM "users" WHERE "is_platform_admin" = true ON CONFLICT ("email") DO NOTHING; UPDATE "users" SET "is_platform_admin" = false WHERE "is_platform_admin" = true; -- platform_admins ist plattformweit (kein tenant_id) → RLS bewusst NICHT aktiviert. -- Zugriff für die eingeschränkte App-Rolle (Härtungs-Iteration) explizit gewähren. GRANT SELECT, INSERT, UPDATE, DELETE ON "platform_admins" TO isms_app;