Files
craftvia/prisma/migrations/0001_baseline/migration.sql
T
msolarczekandClaude Opus 5 1701db0a62 Fundament: Doku, Testanpassungen, Restbereinigung
- AGENTS.md und README.md auf Craftvia umgeschrieben (Regeln, Andockpunkte,
  Stack mit Garage, tsx-Tests, Gate, Demo-Logins)
- ISMS-Dokumente entfernt (SPEC, Prototypen, Lane-Prompts, Übergaben, Konzepte
  Incidents/Framework); Fundament-Doku (Deploy, Sicherheit, Backup, Identity) bleibt
- Fundament-Tests an Craftvia-Rollen/Branding angepasst, Resttreffer
  certvia/isms in Skripten und Kommentaren bereinigt

Gate: prisma generate, migrate status, tsc, lint, build, 22/22 Testskripte grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 11:44:58 +02:00

503 lines
17 KiB
PL/PgSQL

-- CreateSchema
CREATE SCHEMA IF NOT EXISTS "public";
-- CreateExtension
CREATE EXTENSION IF NOT EXISTS "vector";
-- CreateEnum
CREATE TYPE "TenantStatus" AS ENUM ('ACTIVE', 'SUSPENDED', 'ARCHIVED');
-- CreateEnum
CREATE TYPE "UserStatus" AS ENUM ('ACTIVE', 'INVITED', 'LOCKED', 'DEACTIVATED');
-- CreateTable
CREATE TABLE "tenants" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"short" TEXT,
"sector" TEXT,
"status" "TenantStatus" NOT NULL DEFAULT 'ACTIVE',
"config" JSONB NOT NULL DEFAULT '{}',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "tenants_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "tenant_settings" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"org_name" TEXT NOT NULL,
"org_short" TEXT,
"address" TEXT,
"phone" TEXT,
"email" TEXT,
"sector" TEXT,
"logo_key" TEXT,
"accent" TEXT,
"locale" TEXT NOT NULL DEFAULT 'de',
"timezone" TEXT NOT NULL DEFAULT 'Europe/Berlin',
"security_policy" JSONB NOT NULL DEFAULT '{}',
"smtp" JSONB NOT NULL DEFAULT '{}',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "tenant_settings_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "tenant_modules" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"module_key" TEXT NOT NULL,
"enabled" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "tenant_modules_pkey" PRIMARY KEY ("id")
);
-- CreateTable
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',
"ui_locale" TEXT NOT NULL DEFAULT 'de',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "identities_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"identity_id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"name" TEXT NOT NULL,
"status" "UserStatus" NOT NULL DEFAULT 'ACTIVE',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "webauthn_credentials" (
"id" TEXT NOT NULL,
"identity_id" TEXT NOT NULL,
"credential_id" TEXT NOT NULL,
"public_key" TEXT NOT NULL,
"counter" BIGINT NOT NULL DEFAULT 0,
"transports" TEXT[] DEFAULT ARRAY[]::TEXT[],
"device_name" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"last_used_at" TIMESTAMP(3),
CONSTRAINT "webauthn_credentials_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "roles" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"key" TEXT NOT NULL,
"name" TEXT NOT NULL,
CONSTRAINT "roles_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "permissions" (
"id" TEXT NOT NULL,
"key" TEXT NOT NULL,
CONSTRAINT "permissions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "user_roles" (
"user_id" TEXT NOT NULL,
"role_id" TEXT NOT NULL,
CONSTRAINT "user_roles_pkey" PRIMARY KEY ("user_id","role_id")
);
-- CreateTable
CREATE TABLE "role_permissions" (
"role_id" TEXT NOT NULL,
"permission_id" TEXT NOT NULL,
CONSTRAINT "role_permissions_pkey" PRIMARY KEY ("role_id","permission_id")
);
-- 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',
"role" TEXT NOT NULL DEFAULT 'full',
"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_totp_step" BIGINT,
"last_login_at" TIMESTAMP(3),
"sessions_valid_after" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "platform_admins_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "platform_settings" (
"id" TEXT NOT NULL DEFAULT 'singleton',
"mfa_required" BOOLEAN NOT NULL DEFAULT false,
"backup_target" TEXT NOT NULL DEFAULT 'local',
"backup_local_dir" TEXT,
"backup_s3_endpoint" TEXT,
"backup_s3_bucket" TEXT,
"backup_s3_region" TEXT,
"backup_s3_access_key" TEXT,
"backup_s3_secret_key_enc" TEXT,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "platform_settings_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "audit_logs" (
"id" TEXT NOT NULL,
"tenant_id" TEXT,
"scope" TEXT NOT NULL DEFAULT 'tenant',
"actor_id" TEXT,
"action" TEXT NOT NULL,
"entity" TEXT NOT NULL,
"entity_id" TEXT,
"before" JSONB,
"after" JSONB,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "audit_logs_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "mail_logs" (
"id" TEXT NOT NULL,
"tenant_id" TEXT,
"scope" TEXT NOT NULL DEFAULT 'tenant',
"to" TEXT NOT NULL,
"template" TEXT NOT NULL,
"locale" TEXT NOT NULL DEFAULT 'de',
"status" TEXT NOT NULL DEFAULT 'pending',
"provider_message_id" TEXT,
"error" TEXT,
"attempts" INTEGER NOT NULL DEFAULT 0,
"dedupe_key" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"sent_at" TIMESTAMP(3),
CONSTRAINT "mail_logs_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "notification_preferences" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"event_type" TEXT NOT NULL,
"email" BOOLEAN NOT NULL DEFAULT true,
"locale" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "notification_preferences_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "auth_tokens" (
"id" TEXT NOT NULL,
"principal_type" TEXT NOT NULL,
"principal_id" TEXT NOT NULL,
"tenant_id" TEXT,
"type" TEXT NOT NULL,
"token_hash" TEXT NOT NULL,
"new_email" TEXT,
"expires_at" TIMESTAMP(3) NOT NULL,
"used_at" TIMESTAMP(3),
"request_ip" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "auth_tokens_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "deletion_certificates" (
"id" TEXT NOT NULL,
"scope" TEXT NOT NULL,
"tenant_id" TEXT,
"tenant_slug" TEXT,
"subject_identity_id" TEXT,
"subject_email" TEXT,
"actor_id" TEXT,
"deleted_counts" JSONB NOT NULL DEFAULT '{}',
"anonymized_counts" JSONB NOT NULL DEFAULT '{}',
"snapshot_id" TEXT,
"reason" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "deletion_certificates_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "tombstone_entries" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"model" TEXT NOT NULL,
"target_field" TEXT NOT NULL,
"target_value" TEXT NOT NULL,
"action" TEXT NOT NULL,
"anonymized_fields" JSONB NOT NULL DEFAULT '{}',
"reason" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "tombstone_entries_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "backup_jobs" (
"id" TEXT NOT NULL,
"kind" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'queued',
"tenant_id" TEXT NOT NULL,
"tenant_slug" TEXT,
"subject_identity_id" TEXT,
"actor_id" TEXT,
"snapshot_id" TEXT,
"params" JSONB NOT NULL DEFAULT '{}',
"result" JSONB,
"error" TEXT,
"download_token" TEXT,
"download_expires_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "backup_jobs_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "tenants_slug_key" ON "tenants"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "tenant_settings_tenant_id_key" ON "tenant_settings"("tenant_id");
-- CreateIndex
CREATE INDEX "tenant_settings_tenant_id_idx" ON "tenant_settings"("tenant_id");
-- CreateIndex
CREATE INDEX "tenant_modules_tenant_id_idx" ON "tenant_modules"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "tenant_modules_tenant_id_module_key_key" ON "tenant_modules"("tenant_id", "module_key");
-- CreateIndex
CREATE UNIQUE INDEX "identities_email_key" ON "identities"("email");
-- CreateIndex
CREATE INDEX "users_tenant_id_idx" ON "users"("tenant_id");
-- CreateIndex
CREATE INDEX "users_identity_id_idx" ON "users"("identity_id");
-- CreateIndex
CREATE UNIQUE INDEX "users_tenant_id_email_key" ON "users"("tenant_id", "email");
-- CreateIndex
CREATE UNIQUE INDEX "users_tenant_id_identity_id_key" ON "users"("tenant_id", "identity_id");
-- CreateIndex
CREATE UNIQUE INDEX "webauthn_credentials_credential_id_key" ON "webauthn_credentials"("credential_id");
-- CreateIndex
CREATE INDEX "webauthn_credentials_identity_id_idx" ON "webauthn_credentials"("identity_id");
-- CreateIndex
CREATE INDEX "roles_tenant_id_idx" ON "roles"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "roles_tenant_id_key_key" ON "roles"("tenant_id", "key");
-- CreateIndex
CREATE UNIQUE INDEX "permissions_key_key" ON "permissions"("key");
-- CreateIndex
CREATE UNIQUE INDEX "platform_admins_email_key" ON "platform_admins"("email");
-- CreateIndex
CREATE INDEX "audit_logs_tenant_id_entity_entity_id_idx" ON "audit_logs"("tenant_id", "entity", "entity_id");
-- CreateIndex
CREATE INDEX "audit_logs_tenant_id_created_at_idx" ON "audit_logs"("tenant_id", "created_at");
-- CreateIndex
CREATE UNIQUE INDEX "mail_logs_dedupe_key_key" ON "mail_logs"("dedupe_key");
-- CreateIndex
CREATE INDEX "mail_logs_tenant_id_created_at_idx" ON "mail_logs"("tenant_id", "created_at");
-- CreateIndex
CREATE INDEX "mail_logs_status_idx" ON "mail_logs"("status");
-- CreateIndex
CREATE INDEX "notification_preferences_tenant_id_idx" ON "notification_preferences"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "notification_preferences_user_id_event_type_key" ON "notification_preferences"("user_id", "event_type");
-- CreateIndex
CREATE UNIQUE INDEX "auth_tokens_token_hash_key" ON "auth_tokens"("token_hash");
-- CreateIndex
CREATE INDEX "auth_tokens_principal_type_principal_id_type_idx" ON "auth_tokens"("principal_type", "principal_id", "type");
-- CreateIndex
CREATE INDEX "auth_tokens_expires_at_idx" ON "auth_tokens"("expires_at");
-- CreateIndex
CREATE INDEX "deletion_certificates_tenant_id_idx" ON "deletion_certificates"("tenant_id");
-- CreateIndex
CREATE INDEX "deletion_certificates_subject_identity_id_idx" ON "deletion_certificates"("subject_identity_id");
-- CreateIndex
CREATE INDEX "tombstone_entries_tenant_id_idx" ON "tombstone_entries"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "tombstone_entries_tenant_id_model_target_value_key" ON "tombstone_entries"("tenant_id", "model", "target_value");
-- CreateIndex
CREATE UNIQUE INDEX "backup_jobs_download_token_key" ON "backup_jobs"("download_token");
-- CreateIndex
CREATE INDEX "backup_jobs_tenant_id_idx" ON "backup_jobs"("tenant_id");
-- CreateIndex
CREATE INDEX "backup_jobs_status_idx" ON "backup_jobs"("status");
-- AddForeignKey
ALTER TABLE "tenant_settings" ADD CONSTRAINT "tenant_settings_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tenant_modules" ADD CONSTRAINT "tenant_modules_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "users" ADD CONSTRAINT "users_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "users" ADD CONSTRAINT "users_identity_id_fkey" FOREIGN KEY ("identity_id") REFERENCES "identities"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "webauthn_credentials" ADD CONSTRAINT "webauthn_credentials_identity_id_fkey" FOREIGN KEY ("identity_id") REFERENCES "identities"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "roles" ADD CONSTRAINT "roles_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "roles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "role_permissions" ADD CONSTRAINT "role_permissions_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "roles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "role_permissions" ADD CONSTRAINT "role_permissions_permission_id_fkey" FOREIGN KEY ("permission_id") REFERENCES "permissions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- 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;
-- ============================================================================
-- Row Level Security (Craftvia-Baseline)
-- ============================================================================
-- Zweite Verteidigungslinie hinter dem Tenant-Guard (src/server/db.ts).
--
-- App-Rolle `craftvia_app` (NOLOGIN, NOBYPASSRLS): Mit RLS_ENFORCED=true verbindet sich
-- die App als diese Rolle (RLS_DATABASE_URL) und setzt `app.tenant_id` transaktionslokal.
-- LOGIN + Passwort sind ein SECRET und werden NICHT hier gesetzt, sondern operativ:
-- ALTER ROLE craftvia_app WITH LOGIN PASSWORD '<stark>';
--
-- Owner-/Migrationsrolle: Superuser bzw. BYPASSRLS — RLS greift für sie auch unter FORCE
-- nie. Migrationen, Seed und der mandantenübergreifende Login-Lookup bleiben funktionsfähig.
--
-- Nullable tenant_id (audit_logs, mail_logs, auth_tokens): identische Policy. Zeilen mit
-- tenant_id = NULL (Plattform-Ereignisse) erfüllen die Bedingung nie und sind für
-- craftvia_app weder les- noch schreibbar — sie werden ausschließlich über den Owner-Client
-- geschrieben (beabsichtigt, unverändert aus dem Fundament).
--
-- Neue Tenant-Tabellen in künftigen Migrationen: `SELECT enable_tenant_rls('<tabelle>');`
-- (siehe docs/craftvia/MIGRATIONS.md).
-- ============================================================================
DO $$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'craftvia_app') THEN
CREATE ROLE craftvia_app NOLOGIN NOBYPASSRLS;
END IF;
END
$$;
GRANT USAGE ON SCHEMA public TO craftvia_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO craftvia_app;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO craftvia_app;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO craftvia_app;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT USAGE, SELECT ON SEQUENCES TO craftvia_app;
-- Wiederverwendbar und idempotent: ENABLE + Policy (USING + WITH CHECK) + FORCE.
-- WITH CHECK ist zwingend: ohne sie lehnt Postgres unter FORCE jeden INSERT/UPDATE der
-- App-Rolle ab bzw. erlaubt das Umschreiben einer Zeile auf einen fremden Mandanten.
-- Ungesetzter Kontext → current_setting(..., true) = NULL → 0 Zeilen (fail-closed).
CREATE OR REPLACE FUNCTION enable_tenant_rls(tbl text) RETURNS void
LANGUAGE plpgsql AS $fn$
BEGIN
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', tbl);
EXECUTE format('DROP POLICY IF EXISTS tenant_isolation ON %I', tbl);
EXECUTE format(
'CREATE POLICY tenant_isolation ON %I '
|| 'USING (tenant_id = current_setting(''app.tenant_id'', true)) '
|| 'WITH CHECK (tenant_id = current_setting(''app.tenant_id'', true))',
tbl
);
EXECUTE format('ALTER TABLE %I FORCE ROW LEVEL SECURITY', tbl);
EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON %I TO craftvia_app', tbl);
END
$fn$;
-- Tenant-Tabellen (Spiegel von TENANT_MODELS in src/server/db.ts / backup/topology.ts)
SELECT enable_tenant_rls('users');
SELECT enable_tenant_rls('roles');
SELECT enable_tenant_rls('audit_logs');
SELECT enable_tenant_rls('mail_logs');
SELECT enable_tenant_rls('notification_preferences');
SELECT enable_tenant_rls('auth_tokens');
SELECT enable_tenant_rls('tenant_settings');
SELECT enable_tenant_rls('tenant_modules');