-- 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 ''; -- -- 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('');` -- (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');