diff --git a/apps/api/migrations/20260930192133_add_core_users_sso_connections/migration.sql b/apps/api/migrations/20260930192133_add_core_users_sso_connections/migration.sql new file mode 100644 index 000000000..2d6efbcfd --- /dev/null +++ b/apps/api/migrations/20260930192133_add_core_users_sso_connections/migration.sql @@ -0,0 +1,41 @@ +CREATE TABLE "core_users_sso_operations" ( + "id" serial PRIMARY KEY, + "tokenHash" varchar(64) UNIQUE, + "userId" integer NOT NULL, + "providerId" varchar(255) NOT NULL, + "intent" varchar(16) NOT NULL, + "fields" varchar(32)[] DEFAULT '{}'::varchar(32)[] NOT NULL, + "providerAccountId" varchar(255), + "preview" jsonb, + "createdAt" timestamp DEFAULT now() NOT NULL, + "expiresAt" timestamp NOT NULL, + CONSTRAINT "core_users_sso_operations_intent_check" CHECK ("intent" IN ('link', 'import', 'preview')) +); +--> statement-breakpoint +ALTER TABLE "core_users_sso_operations" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint +CREATE TABLE "core_users_sso_profile_sources" ( + "userId" integer, + "field" varchar(32), + "providerId" varchar(255) NOT NULL, + "createdAt" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "core_users_sso_profile_sources_pkey" PRIMARY KEY("userId","field"), + CONSTRAINT "core_users_sso_profile_sources_field_check" CHECK ("field" IN ('avatar', 'firstName', 'lastName')) +); +--> statement-breakpoint +ALTER TABLE "core_users_sso_profile_sources" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint +ALTER TABLE "core_users_sso" ADD COLUMN "providerEmail" varchar(255);--> statement-breakpoint +ALTER TABLE "core_users_sso" ADD COLUMN "providerUsername" varchar(255);--> statement-breakpoint +ALTER TABLE "core_users_sso" ADD COLUMN "syncOnSignIn" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "core_users_sso" ADD COLUMN "avatarSourceUrl" varchar(2048);--> statement-breakpoint +ALTER TABLE "core_users_sso" ADD COLUMN "avatarSha256" varchar(64);--> statement-breakpoint +ALTER TABLE "core_users_sso" ADD COLUMN "avatarFileId" integer;--> statement-breakpoint +DELETE FROM "core_users_sso" AS "duplicate" USING "core_users_sso" AS "kept" WHERE "duplicate"."providerId" = "kept"."providerId" AND "duplicate"."providerAccountId" = "kept"."providerAccountId" AND ("duplicate"."createdAt", "duplicate".ctid) > ("kept"."createdAt", "kept".ctid);--> statement-breakpoint +DELETE FROM "core_users_sso" AS "duplicate" USING "core_users_sso" AS "kept" WHERE "duplicate"."userId" = "kept"."userId" AND "duplicate"."providerId" = "kept"."providerId" AND ("duplicate"."createdAt", "duplicate".ctid) > ("kept"."createdAt", "kept".ctid);--> statement-breakpoint +ALTER TABLE "core_users_sso" ADD CONSTRAINT "core_users_sso_provider_account_key" UNIQUE("providerId","providerAccountId");--> statement-breakpoint +ALTER TABLE "core_users_sso" ADD CONSTRAINT "core_users_sso_user_provider_key" UNIQUE("userId","providerId");--> statement-breakpoint +CREATE INDEX "core_users_sso_operations_user_provider_idx" ON "core_users_sso_operations" ("userId","providerId");--> statement-breakpoint +CREATE INDEX "core_users_sso_operations_expires_at_idx" ON "core_users_sso_operations" ("expiresAt");--> statement-breakpoint +ALTER TABLE "core_users_sso" ADD CONSTRAINT "core_users_sso_avatarFileId_core_files_id_fkey" FOREIGN KEY ("avatarFileId") REFERENCES "core_files"("id") ON DELETE SET NULL;--> statement-breakpoint +ALTER TABLE "core_users_sso_operations" ADD CONSTRAINT "core_users_sso_operations_userId_core_users_id_fkey" FOREIGN KEY ("userId") REFERENCES "core_users"("id") ON DELETE CASCADE;--> statement-breakpoint +ALTER TABLE "core_users_sso_profile_sources" ADD CONSTRAINT "core_users_sso_profile_sources_userId_core_users_id_fkey" FOREIGN KEY ("userId") REFERENCES "core_users"("id") ON DELETE CASCADE;--> statement-breakpoint +ALTER TABLE "core_users_sso_profile_sources" ADD CONSTRAINT "core_users_sso_profile_sources_connection_fkey" FOREIGN KEY ("userId","providerId") REFERENCES "core_users_sso"("userId","providerId") ON DELETE CASCADE; \ No newline at end of file diff --git a/apps/api/migrations/20260930192133_add_core_users_sso_connections/snapshot.json b/apps/api/migrations/20260930192133_add_core_users_sso_connections/snapshot.json new file mode 100644 index 000000000..d7dc32e26 --- /dev/null +++ b/apps/api/migrations/20260930192133_add_core_users_sso_connections/snapshot.json @@ -0,0 +1,9665 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "ffba1781-4577-4084-998a-f9a6ce6e5d1f", + "prevIds": [ + "c1eb3581-341a-484b-92c7-eedd030ab3c9" + ], + "ddl": [ + { + "isRlsEnabled": true, + "name": "core_admin_permissions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_admin_sessions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_file_refs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_revisions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_schedules", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_slug_history", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_cron", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_admin_dashboard", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_files", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_languages", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_languages_words", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_logs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_moderators_permissions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_navigation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_page_layouts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_passkey_challenges", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_passkeys", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_queue", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_roles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_search_index", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_secrets", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_sessions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_sessions_known_devices", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_confirm_emails", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_forgot_password", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_secondary_roles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_sso", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_sso_operations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_sso_profile_sources", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_categories", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_categories_translations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_posts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_posts_author_id", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_posts_category_id", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_posts_translations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_advanced_articles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_advanced_articles_categories", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_advanced_articles_faq", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_advanced_articles_related_articles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_advanced_articles_translations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_articles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_articles_gallery", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_categories", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_localized_articles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_localized_articles_translations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_pages", + "entityType": "tables", + "schema": "public" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "roleId", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "protected", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "unrestricted", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "permissions", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "lastSeen", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deviceId", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "revisionId", + "entityType": "columns", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileId", + "entityType": "columns", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "operation", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "snapshot", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "changedFields", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'system'", + "generated": null, + "identity": null, + "name": "actorType", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actorUserId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "restoredFromRevisionId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "action", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scheduledFor", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "generation", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "createdBy", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "effectsError", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "varchar(512)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "path", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "retiredAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastRun", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "module", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nextRun", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "schedule", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "widgets", + "entityType": "columns", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(512)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folder", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mimeType", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "size", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "code", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'UTC'", + "generated": null, + "identity": null, + "name": "timezone", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "protected", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "default", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "time24", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageCode", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginCode", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tableName", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "variable", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "varchar(10)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "varchar(45)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "varchar(10)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'GET'", + "generated": null, + "identity": null, + "name": "method", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'localhost'", + "generated": null, + "identity": null, + "name": "path", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userAgent", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "500", + "generated": null, + "identity": null, + "name": "statusCode", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "test123", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "roleId", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "protected", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "unrestricted", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "permissions", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "parentId", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'header'", + "generated": null, + "identity": null, + "name": "location", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "varchar(120)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "presetId", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "href", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "icon", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isOpenInNewTab", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "varchar(120)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pageId", + "entityType": "columns", + "schema": "public", + "table": "core_page_layouts" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "zones", + "entityType": "columns", + "schema": "public", + "table": "core_page_layouts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_page_layouts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_page_layouts" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenHash", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ceremony", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "challenge", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "webauthnUserId", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(1024)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "credentialId", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publicKey", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "counter", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "webauthnUserId", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "transports", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deviceType", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "backedUp", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(36)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "aaguid", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastUsedAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'default'", + "generated": null, + "identity": null, + "name": "queue", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "payload", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "priority", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "attempts", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "3", + "generated": null, + "identity": null, + "name": "maxAttempts", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "availableAt", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reservedAt", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completedAt", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "protected", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "default", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "root", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "guest", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "color", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prefix", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "allowUploadFiles", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "totalMaxStorage", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxStorageForSubmit", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "allowUploadAvatar", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "2048", + "generated": null, + "identity": null, + "name": "maxAvatarSize", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "allowUploadCover", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "5120", + "generated": null, + "identity": null, + "name": "maxCoverSize", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "allowEditPersonalInfo", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemType", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "languageCode", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "authorIds", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "tsvector", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": { + "as": "setweight(to_tsvector(CASE lower(split_part(\"core_search_index\".\"languageCode\", '-', 1)) WHEN 'da' THEN 'danish'::regconfig WHEN 'de' THEN 'german'::regconfig WHEN 'en' THEN 'english'::regconfig WHEN 'es' THEN 'spanish'::regconfig WHEN 'fi' THEN 'finnish'::regconfig WHEN 'fr' THEN 'french'::regconfig WHEN 'hu' THEN 'hungarian'::regconfig WHEN 'it' THEN 'italian'::regconfig WHEN 'nl' THEN 'dutch'::regconfig WHEN 'no' THEN 'norwegian'::regconfig WHEN 'pl' THEN 'polish'::regconfig WHEN 'pt' THEN 'portuguese'::regconfig WHEN 'ro' THEN 'romanian'::regconfig WHEN 'ru' THEN 'russian'::regconfig WHEN 'sv' THEN 'swedish'::regconfig WHEN 'tr' THEN 'turkish'::regconfig ELSE 'simple'::regconfig END, coalesce(\"core_search_index\".\"title\", '')), 'A') || setweight(to_tsvector(CASE lower(split_part(\"core_search_index\".\"languageCode\", '-', 1)) WHEN 'da' THEN 'danish'::regconfig WHEN 'de' THEN 'german'::regconfig WHEN 'en' THEN 'english'::regconfig WHEN 'es' THEN 'spanish'::regconfig WHEN 'fi' THEN 'finnish'::regconfig WHEN 'fr' THEN 'french'::regconfig WHEN 'hu' THEN 'hungarian'::regconfig WHEN 'it' THEN 'italian'::regconfig WHEN 'nl' THEN 'dutch'::regconfig WHEN 'no' THEN 'norwegian'::regconfig WHEN 'pl' THEN 'polish'::regconfig WHEN 'pt' THEN 'portuguese'::regconfig WHEN 'ro' THEN 'romanian'::regconfig WHEN 'ru' THEN 'russian'::regconfig WHEN 'sv' THEN 'swedish'::regconfig WHEN 'tr' THEN 'turkish'::regconfig ELSE 'simple'::regconfig END, coalesce(\"core_search_index\".\"content\", '')), 'B')", + "type": "stored" + }, + "identity": null, + "name": "search_vector", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "containerType", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "containerId", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "isPublic", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "indexedAt", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_secrets" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "core_secrets" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_secrets" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deviceId", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publicId", + "entityType": "columns", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "type": "varchar(40)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userAgent", + "entityType": "columns", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "lastSeen", + "entityType": "columns", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nameCode", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "firstName", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastName", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "phone", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "headline", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "showRealName", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "password", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "newsletter", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(6)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatarColor", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "emailVerified", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "roleId", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "birthday", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(40)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'en'", + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatarId", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "coverId", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "varchar(40)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "varchar(40)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "roleId", + "entityType": "columns", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerAccountId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerEmail", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerUsername", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "syncOnSignIn", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatarSourceUrl", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatarSha256", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatarFileId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenHash", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "intent", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "fields", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerAccountId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "preview", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "field", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "blog_categories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_categories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_categories" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "color", + "entityType": "columns", + "schema": "public", + "table": "blog_categories" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "coverImage", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "friendlyUrl", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "coverImageAlt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "syndicationIndexable", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "syndicationNoIndex", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "5", + "generated": null, + "identity": null, + "name": "syndicationPriority", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_categories" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_categories" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_categories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_categories" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_faq" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_faq" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_faq" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_faq" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_faq" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "question", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_faq" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "answer", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_faq" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_related_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_related_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_related_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_related_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "seoTitle", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "seoDescription", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "code", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "excerpt", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "views", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "featured", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "noIndex", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "author", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "animation", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "category", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "example_articles_gallery" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "example_articles_gallery" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "example_articles_gallery" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_articles_gallery" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "example_categories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_categories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_categories" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "example_categories" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "featured", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "example_pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "example_pages" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "example_pages" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "example_pages" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "example_pages" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "example_pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "roleId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_permissions_role_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_permissions_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_permissions_updated_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "token", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_sessions_token_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_sessions_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "revisionId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "fileId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_file_refs_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "fileId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_file_refs_file_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "version", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"languageId\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_revisions_item_version_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "version", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"languageId\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_revisions_translation_version_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "version", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_revisions_language_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_revisions_plugin_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "actorUserId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_revisions_actor_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "action", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "status = 'pending'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_schedules_active_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "scheduledFor", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_schedules_due_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_schedules_item_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_schedules_plugin_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_schedules_created_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"languageId\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_slug_history_shared_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"languageId\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_slug_history_locale_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_slug_history_item_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_slug_history_plugin_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "lastRun", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_cron_last_run_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_cron" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_dashboard_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_files_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_files_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "code", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_languages_code_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_languages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_languages_name_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_languages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageCode", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_languages_words_lang_code_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_languages_words" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_logs_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_logs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "roleId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderators_permissions_role_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderators_permissions_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderators_permissions_updated_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_navigation_position_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_navigation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "location", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_navigation_location_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_navigation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "parentId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_navigation_parent_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_navigation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "expiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_passkey_challenges_expires_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_passkey_challenges_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_passkeys_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "webauthnUserId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_passkeys_webauthn_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "availableAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_queue_status_available_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_queue" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_queue_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_queue" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_roles_updated_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_roles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "search_vector", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "core_search_index_search_vector_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_index_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "authorIds", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "core_search_index_author_ids_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_index_item_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageCode", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_index_language_code_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "isPublic", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_index_is_public_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_sessions_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "ipAddress", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_sessions_known_devices_ip_address_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "nameCode", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_name_code_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_name_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_email_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "avatarId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_avatar_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "coverId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_cover_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_secondary_roles_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "roleId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_secondary_roles_role_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_sso_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_sso" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "providerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_sso_operations_user_provider_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "expiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_sso_operations_expires_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_categories_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_categories_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_categories_translations_language_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_status_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "coverImage", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_cover_image_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_author_id_position_key", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_author_id_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_category_id_position_key", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_category_id_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_translations_language_id_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "friendlyUrl", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_translations_language_id_friendly_url_key", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "syndicationPriority", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_syndication_priority_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_categories_position_key", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_categories_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_faq_position_key", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles_faq" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_related_articles_position_key", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles_related_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_related_articles_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles_related_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_translations_language_id_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_translations_language_id_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_status_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "code", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_code_key", + "entityType": "indexes", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "author", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_author_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "animation", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_animation_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "category", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_category_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_gallery_position_key", + "entityType": "indexes", + "schema": "public", + "table": "example_articles_gallery" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_gallery_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_articles_gallery" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_categories_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_categories_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_localized_articles_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_localized_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_localized_articles_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_localized_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_localized_articles_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_localized_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_localized_articles_translations_language_id_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_localized_articles_translations_language_id_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_pages_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "example_pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_pages_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_pages_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_pages_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_pages" + }, + { + "nameExplicit": false, + "columns": [ + "roleId" + ], + "schemaTo": "public", + "tableTo": "core_roles", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_admin_permissions_roleId_core_roles_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_admin_permissions_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_admin_sessions_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "nameExplicit": false, + "columns": [ + "deviceId" + ], + "schemaTo": "public", + "tableTo": "core_sessions_known_devices", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_admin_sessions_deviceId_core_sessions_known_devices_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "nameExplicit": false, + "columns": [ + "revisionId" + ], + "schemaTo": "public", + "tableTo": "core_content_revisions", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "core_content_file_refs_5get6SfhBPHr_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "nameExplicit": false, + "columns": [ + "fileId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "core_content_file_refs_fileId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "nameExplicit": false, + "columns": [ + "actorUserId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "core_content_revisions_actorUserId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": false, + "columns": [ + "createdBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "core_content_schedules_createdBy_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_admin_dashboard_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_files_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_files" + }, + { + "nameExplicit": false, + "columns": [ + "languageCode" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "code" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_languages_words_languageCode_core_languages_code_fk", + "entityType": "fks", + "schema": "public", + "table": "core_languages_words" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "core_logs_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_logs" + }, + { + "nameExplicit": false, + "columns": [ + "roleId" + ], + "schemaTo": "public", + "tableTo": "core_roles", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_moderators_permissions_roleId_core_roles_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_moderators_permissions_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "nameExplicit": false, + "columns": [ + "parentId" + ], + "schemaTo": "public", + "tableTo": "core_navigation", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_navigation_parentId_core_navigation_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_navigation" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_passkey_challenges_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_passkeys_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_sessions_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_sessions" + }, + { + "nameExplicit": false, + "columns": [ + "deviceId" + ], + "schemaTo": "public", + "tableTo": "core_sessions_known_devices", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_sessions_deviceId_core_sessions_known_devices_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_sessions" + }, + { + "nameExplicit": false, + "columns": [ + "roleId" + ], + "schemaTo": "public", + "tableTo": "core_roles", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "core_users_roleId_core_roles_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": false, + "columns": [ + "language" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "code" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET DEFAULT", + "name": "core_users_language_core_languages_code_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": false, + "columns": [ + "avatarId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_users_avatarId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": false, + "columns": [ + "coverId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_users_coverId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_confirm_emails_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_forgot_password_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_secondary_roles_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "nameExplicit": false, + "columns": [ + "roleId" + ], + "schemaTo": "public", + "tableTo": "core_roles", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_secondary_roles_roleId_core_roles_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_sso_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users_sso" + }, + { + "nameExplicit": false, + "columns": [ + "avatarFileId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_users_sso_avatarFileId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_sso" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_sso_operations_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_sso_profile_sources_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "nameExplicit": true, + "columns": [ + "userId", + "providerId" + ], + "schemaTo": "public", + "tableTo": "core_users_sso", + "columnsTo": [ + "userId", + "providerId" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_sso_profile_sources_connection_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "blog_categories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "blog_categories_translations_itemId_blog_categories_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "nameExplicit": false, + "columns": [ + "languageId" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "blog_categories_translations_languageId_core_languages_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "nameExplicit": false, + "columns": [ + "coverImage" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "blog_posts_coverImage_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "blog_posts", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "blog_posts_author_id_itemId_blog_posts_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "blog_posts_author_id_relatedItemId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "blog_posts", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "blog_posts_category_id_itemId_blog_posts_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "blog_categories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "blog_posts_category_id_relatedItemId_blog_categories_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "blog_posts", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "blog_posts_translations_itemId_blog_posts_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "nameExplicit": false, + "columns": [ + "languageId" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "blog_posts_translations_languageId_core_languages_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "example_advanced_articles", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_advanced_articles_categories_itemId_example_advanced_ar", + "entityType": "fks", + "schema": "public", + "table": "example_advanced_articles_categories" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "example_categories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "example_advanced_articles_categories_relatedItemId_example_cate", + "entityType": "fks", + "schema": "public", + "table": "example_advanced_articles_categories" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "example_advanced_articles", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_advanced_articles_faq_itemId_example_advanced_articles_", + "entityType": "fks", + "schema": "public", + "table": "example_advanced_articles_faq" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "example_advanced_articles", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_advanced_articles_related_articles_itemId_example_advan", + "entityType": "fks", + "schema": "public", + "table": "example_advanced_articles_related_articles" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "example_advanced_articles", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_advanced_articles_related_articles_relatedItemId_exampl", + "entityType": "fks", + "schema": "public", + "table": "example_advanced_articles_related_articles" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "example_advanced_articles", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_advanced_articles_translations_itemId_example_advanced_", + "entityType": "fks", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "nameExplicit": false, + "columns": [ + "languageId" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "example_advanced_articles_translations_languageId_core_language", + "entityType": "fks", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "nameExplicit": false, + "columns": [ + "author" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "example_articles_author_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": false, + "columns": [ + "animation" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "example_articles_animation_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": false, + "columns": [ + "category" + ], + "schemaTo": "public", + "tableTo": "example_categories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "example_articles_category_example_categories_id_fk", + "entityType": "fks", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "example_articles", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_articles_gallery_itemId_example_articles_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_articles_gallery" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "example_articles_gallery_relatedItemId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_articles_gallery" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "example_localized_articles", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_localized_articles_translations_itemId_example_localize", + "entityType": "fks", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "nameExplicit": false, + "columns": [ + "languageId" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "example_localized_articles_translations_languageId_core_languag", + "entityType": "fks", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "columns": [ + "userId", + "roleId" + ], + "nameExplicit": false, + "name": "core_users_secondary_roles_userId_roleId_pk", + "entityType": "pks", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "columns": [ + "userId", + "field" + ], + "nameExplicit": false, + "name": "core_users_sso_profile_sources_pkey", + "entityType": "pks", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "columns": [ + "itemId", + "languageId" + ], + "nameExplicit": true, + "name": "blog_categories_translations_item_id_language_id_pk", + "entityType": "pks", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "blog_posts_author_id_pk", + "entityType": "pks", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "blog_posts_category_id_pk", + "entityType": "pks", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "columns": [ + "itemId", + "languageId" + ], + "nameExplicit": true, + "name": "blog_posts_translations_item_id_language_id_pk", + "entityType": "pks", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "example_advanced_articles_categories_pk", + "entityType": "pks", + "schema": "public", + "table": "example_advanced_articles_categories" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "example_advanced_articles_related_articles_pk", + "entityType": "pks", + "schema": "public", + "table": "example_advanced_articles_related_articles" + }, + { + "columns": [ + "itemId", + "languageId" + ], + "nameExplicit": true, + "name": "example_advanced_articles_translations_item_id_language_id_pk", + "entityType": "pks", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "example_articles_gallery_pk", + "entityType": "pks", + "schema": "public", + "table": "example_articles_gallery" + }, + { + "columns": [ + "itemId", + "languageId" + ], + "nameExplicit": true, + "name": "example_localized_articles_translations_item_id_language_id_pk", + "entityType": "pks", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_admin_permissions_pkey", + "schema": "public", + "table": "core_admin_permissions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_admin_sessions_pkey", + "schema": "public", + "table": "core_admin_sessions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_file_refs_pkey", + "schema": "public", + "table": "core_content_file_refs", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_revisions_pkey", + "schema": "public", + "table": "core_content_revisions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_schedules_pkey", + "schema": "public", + "table": "core_content_schedules", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_slug_history_pkey", + "schema": "public", + "table": "core_content_slug_history", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_cron_pkey", + "schema": "public", + "table": "core_cron", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_admin_dashboard_pkey", + "schema": "public", + "table": "core_admin_dashboard", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_files_pkey", + "schema": "public", + "table": "core_files", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_languages_pkey", + "schema": "public", + "table": "core_languages", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_languages_words_pkey", + "schema": "public", + "table": "core_languages_words", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_logs_pkey", + "schema": "public", + "table": "core_logs", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_moderators_permissions_pkey", + "schema": "public", + "table": "core_moderators_permissions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_navigation_pkey", + "schema": "public", + "table": "core_navigation", + "entityType": "pks" + }, + { + "columns": [ + "pageId" + ], + "nameExplicit": false, + "name": "core_page_layouts_pkey", + "schema": "public", + "table": "core_page_layouts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_passkey_challenges_pkey", + "schema": "public", + "table": "core_users_passkey_challenges", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_passkeys_pkey", + "schema": "public", + "table": "core_users_passkeys", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_queue_pkey", + "schema": "public", + "table": "core_queue", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_roles_pkey", + "schema": "public", + "table": "core_roles", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_search_index_pkey", + "schema": "public", + "table": "core_search_index", + "entityType": "pks" + }, + { + "columns": [ + "name" + ], + "nameExplicit": false, + "name": "core_secrets_pkey", + "schema": "public", + "table": "core_secrets", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_sessions_pkey", + "schema": "public", + "table": "core_sessions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_sessions_known_devices_pkey", + "schema": "public", + "table": "core_sessions_known_devices", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_pkey", + "schema": "public", + "table": "core_users", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_confirm_emails_pkey", + "schema": "public", + "table": "core_users_confirm_emails", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_forgot_password_pkey", + "schema": "public", + "table": "core_users_forgot_password", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_sso_operations_pkey", + "schema": "public", + "table": "core_users_sso_operations", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "blog_categories_pkey", + "schema": "public", + "table": "blog_categories", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "blog_posts_pkey", + "schema": "public", + "table": "blog_posts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "example_advanced_articles_pkey", + "schema": "public", + "table": "example_advanced_articles", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "example_advanced_articles_faq_pkey", + "schema": "public", + "table": "example_advanced_articles_faq", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "example_articles_pkey", + "schema": "public", + "table": "example_articles", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "example_categories_pkey", + "schema": "public", + "table": "example_categories", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "example_localized_articles_pkey", + "schema": "public", + "table": "example_localized_articles", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "example_pages_pkey", + "schema": "public", + "table": "example_pages", + "entityType": "pks" + }, + { + "nameExplicit": true, + "columns": [ + "itemType", + "itemId", + "languageCode" + ], + "nullsNotDistinct": false, + "name": "core_search_index_item_unique", + "entityType": "uniques", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + "providerId", + "providerAccountId" + ], + "nullsNotDistinct": false, + "name": "core_users_sso_provider_account_key", + "entityType": "uniques", + "schema": "public", + "table": "core_users_sso" + }, + { + "nameExplicit": true, + "columns": [ + "userId", + "providerId" + ], + "nullsNotDistinct": false, + "name": "core_users_sso_user_provider_key", + "entityType": "uniques", + "schema": "public", + "table": "core_users_sso" + }, + { + "nameExplicit": false, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "core_admin_sessions_token_unique", + "schema": "public", + "table": "core_admin_sessions", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "nullsNotDistinct": false, + "name": "core_admin_dashboard_userId_unique", + "schema": "public", + "table": "core_admin_dashboard", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "key" + ], + "nullsNotDistinct": false, + "name": "core_files_key_unique", + "schema": "public", + "table": "core_files", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "code" + ], + "nullsNotDistinct": false, + "name": "core_languages_code_unique", + "schema": "public", + "table": "core_languages", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "tokenHash" + ], + "nullsNotDistinct": false, + "name": "core_users_passkey_challenges_tokenHash_key", + "schema": "public", + "table": "core_users_passkey_challenges", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "credentialId" + ], + "nullsNotDistinct": false, + "name": "core_users_passkeys_credentialId_key", + "schema": "public", + "table": "core_users_passkeys", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "core_sessions_token_unique", + "schema": "public", + "table": "core_sessions", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "publicId" + ], + "nullsNotDistinct": false, + "name": "core_sessions_known_devices_publicId_unique", + "schema": "public", + "table": "core_sessions_known_devices", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "nameCode" + ], + "nullsNotDistinct": false, + "name": "core_users_nameCode_unique", + "schema": "public", + "table": "core_users", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "name" + ], + "nullsNotDistinct": false, + "name": "core_users_name_unique", + "schema": "public", + "table": "core_users", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "email" + ], + "nullsNotDistinct": false, + "name": "core_users_email_unique", + "schema": "public", + "table": "core_users", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "core_users_confirm_emails_token_unique", + "schema": "public", + "table": "core_users_confirm_emails", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "nullsNotDistinct": false, + "name": "core_users_forgot_password_userId_unique", + "schema": "public", + "table": "core_users_forgot_password", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "core_users_forgot_password_token_unique", + "schema": "public", + "table": "core_users_forgot_password", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "tokenHash" + ], + "nullsNotDistinct": false, + "name": "core_users_sso_operations_tokenHash_key", + "schema": "public", + "table": "core_users_sso_operations", + "entityType": "uniques" + }, + { + "value": "\"intent\" IN ('link', 'import', 'preview')", + "name": "core_users_sso_operations_intent_check", + "entityType": "checks", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "value": "\"field\" IN ('avatar', 'firstName', 'lastName')", + "name": "core_users_sso_profile_sources_field_check", + "entityType": "checks", + "schema": "public", + "table": "core_users_sso_profile_sources" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/apps/api/migrations/20260930201557_allow_core_users_sso_sync_intent/migration.sql b/apps/api/migrations/20260930201557_allow_core_users_sso_sync_intent/migration.sql new file mode 100644 index 000000000..5ae182de7 --- /dev/null +++ b/apps/api/migrations/20260930201557_allow_core_users_sso_sync_intent/migration.sql @@ -0,0 +1 @@ +ALTER TABLE "core_users_sso_operations" DROP CONSTRAINT "core_users_sso_operations_intent_check", ADD CONSTRAINT "core_users_sso_operations_intent_check" CHECK ("intent" IN ('link', 'import', 'sync', 'preview')); \ No newline at end of file diff --git a/apps/api/migrations/20260930201557_allow_core_users_sso_sync_intent/snapshot.json b/apps/api/migrations/20260930201557_allow_core_users_sso_sync_intent/snapshot.json new file mode 100644 index 000000000..2b936dc58 --- /dev/null +++ b/apps/api/migrations/20260930201557_allow_core_users_sso_sync_intent/snapshot.json @@ -0,0 +1,9665 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "0c4efb07-e5e5-4e06-b87e-88d08c84a3a8", + "prevIds": [ + "ffba1781-4577-4084-998a-f9a6ce6e5d1f" + ], + "ddl": [ + { + "isRlsEnabled": true, + "name": "core_admin_permissions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_admin_sessions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_file_refs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_revisions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_schedules", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_content_slug_history", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_cron", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_admin_dashboard", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_files", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_languages", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_languages_words", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_logs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_moderators_permissions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_navigation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_page_layouts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_passkey_challenges", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_passkeys", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_queue", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_roles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_search_index", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_secrets", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_sessions", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_sessions_known_devices", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_confirm_emails", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_forgot_password", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_secondary_roles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_sso", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_sso_operations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "core_users_sso_profile_sources", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_categories", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_categories_translations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_posts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_posts_author_id", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_posts_category_id", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "blog_posts_translations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_advanced_articles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_advanced_articles_categories", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_advanced_articles_faq", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_advanced_articles_related_articles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_advanced_articles_translations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_articles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_articles_gallery", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_categories", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_localized_articles", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_localized_articles_translations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": true, + "name": "example_pages", + "entityType": "tables", + "schema": "public" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "roleId", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "protected", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "unrestricted", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "permissions", + "entityType": "columns", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "lastSeen", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deviceId", + "entityType": "columns", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "revisionId", + "entityType": "columns", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "fileId", + "entityType": "columns", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "operation", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "snapshot", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "changedFields", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'system'", + "generated": null, + "identity": null, + "name": "actorType", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "actorUserId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "restoredFromRevisionId", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_revisions" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "action", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scheduledFor", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "generation", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "createdBy", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completedAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "effectsError", + "entityType": "columns", + "schema": "public", + "table": "core_content_schedules" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "contentTypeId", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "varchar(512)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "path", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "retiredAt", + "entityType": "columns", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastRun", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "module", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nextRun", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "schedule", + "entityType": "columns", + "schema": "public", + "table": "core_cron" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "widgets", + "entityType": "columns", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(512)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "folder", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "mimeType", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "size", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_files" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "code", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'UTC'", + "generated": null, + "identity": null, + "name": "timezone", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "protected", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "default", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "time24", + "entityType": "columns", + "schema": "public", + "table": "core_languages" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageCode", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginCode", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tableName", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "variable", + "entityType": "columns", + "schema": "public", + "table": "core_languages_words" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "varchar(10)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "varchar(45)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "varchar(10)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'GET'", + "generated": null, + "identity": null, + "name": "method", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'localhost'", + "generated": null, + "identity": null, + "name": "path", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userAgent", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "500", + "generated": null, + "identity": null, + "name": "statusCode", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "test123", + "entityType": "columns", + "schema": "public", + "table": "core_logs" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "roleId", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "protected", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "unrestricted", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "permissions", + "entityType": "columns", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "parentId", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'header'", + "generated": null, + "identity": null, + "name": "location", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "varchar(120)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "presetId", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "href", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "icon", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "isOpenInNewTab", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_navigation" + }, + { + "type": "varchar(120)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pageId", + "entityType": "columns", + "schema": "public", + "table": "core_page_layouts" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "zones", + "entityType": "columns", + "schema": "public", + "table": "core_page_layouts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_page_layouts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_page_layouts" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenHash", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ceremony", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "challenge", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "webauthnUserId", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(1024)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "credentialId", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publicKey", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "counter", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "webauthnUserId", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "transports", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deviceType", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "backedUp", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(36)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "aaguid", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastUsedAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'default'", + "generated": null, + "identity": null, + "name": "queue", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "payload", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "priority", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "attempts", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "3", + "generated": null, + "identity": null, + "name": "maxAttempts", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "availableAt", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "reservedAt", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastError", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "completedAt", + "entityType": "columns", + "schema": "public", + "table": "core_queue" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "protected", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "default", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "root", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "guest", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "color", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "prefix", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "allowUploadFiles", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "totalMaxStorage", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "maxStorageForSubmit", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "allowUploadAvatar", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "2048", + "generated": null, + "identity": null, + "name": "maxAvatarSize", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "allowUploadCover", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "5120", + "generated": null, + "identity": null, + "name": "maxCoverSize", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "allowEditPersonalInfo", + "entityType": "columns", + "schema": "public", + "table": "core_roles" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "pluginId", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemType", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "languageCode", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "authorIds", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "tsvector", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": { + "as": "setweight(to_tsvector(CASE lower(split_part(\"core_search_index\".\"languageCode\", '-', 1)) WHEN 'da' THEN 'danish'::regconfig WHEN 'de' THEN 'german'::regconfig WHEN 'en' THEN 'english'::regconfig WHEN 'es' THEN 'spanish'::regconfig WHEN 'fi' THEN 'finnish'::regconfig WHEN 'fr' THEN 'french'::regconfig WHEN 'hu' THEN 'hungarian'::regconfig WHEN 'it' THEN 'italian'::regconfig WHEN 'nl' THEN 'dutch'::regconfig WHEN 'no' THEN 'norwegian'::regconfig WHEN 'pl' THEN 'polish'::regconfig WHEN 'pt' THEN 'portuguese'::regconfig WHEN 'ro' THEN 'romanian'::regconfig WHEN 'ru' THEN 'russian'::regconfig WHEN 'sv' THEN 'swedish'::regconfig WHEN 'tr' THEN 'turkish'::regconfig ELSE 'simple'::regconfig END, coalesce(\"core_search_index\".\"title\", '')), 'A') || setweight(to_tsvector(CASE lower(split_part(\"core_search_index\".\"languageCode\", '-', 1)) WHEN 'da' THEN 'danish'::regconfig WHEN 'de' THEN 'german'::regconfig WHEN 'en' THEN 'english'::regconfig WHEN 'es' THEN 'spanish'::regconfig WHEN 'fi' THEN 'finnish'::regconfig WHEN 'fr' THEN 'french'::regconfig WHEN 'hu' THEN 'hungarian'::regconfig WHEN 'it' THEN 'italian'::regconfig WHEN 'nl' THEN 'dutch'::regconfig WHEN 'no' THEN 'norwegian'::regconfig WHEN 'pl' THEN 'polish'::regconfig WHEN 'pt' THEN 'portuguese'::regconfig WHEN 'ro' THEN 'romanian'::regconfig WHEN 'ru' THEN 'russian'::regconfig WHEN 'sv' THEN 'swedish'::regconfig WHEN 'tr' THEN 'turkish'::regconfig ELSE 'simple'::regconfig END, coalesce(\"core_search_index\".\"content\", '')), 'B')", + "type": "stored" + }, + "identity": null, + "name": "search_vector", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "containerType", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "containerId", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "url", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "isPublic", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "indexedAt", + "entityType": "columns", + "schema": "public", + "table": "core_search_index" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_secrets" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "core_secrets" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_secrets" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "deviceId", + "entityType": "columns", + "schema": "public", + "table": "core_sessions" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publicId", + "entityType": "columns", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "type": "varchar(40)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userAgent", + "entityType": "columns", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "lastSeen", + "entityType": "columns", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "nameCode", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "firstName", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "lastName", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "phone", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "headline", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "showRealName", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "password", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "newsletter", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(6)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatarColor", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "emailVerified", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "roleId", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "birthday", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(40)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'en'", + "generated": null, + "identity": null, + "name": "language", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatarId", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "coverId", + "entityType": "columns", + "schema": "public", + "table": "core_users" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "varchar(40)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "varchar(40)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ipAddress", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "roleId", + "entityType": "columns", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerAccountId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerEmail", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerUsername", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "syncOnSignIn", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(2048)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatarSourceUrl", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatarSha256", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "avatarFileId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tokenHash", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "intent", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "fields", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerAccountId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "preview", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expiresAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "userId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "field", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "providerId", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "blog_categories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_categories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_categories" + }, + { + "type": "varchar(50)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "color", + "entityType": "columns", + "schema": "public", + "table": "blog_categories" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "coverImage", + "entityType": "columns", + "schema": "public", + "table": "blog_posts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "friendlyUrl", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "coverImageAlt", + "entityType": "columns", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "syndicationIndexable", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "syndicationNoIndex", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "5", + "generated": null, + "identity": null, + "name": "syndicationPriority", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_categories" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_categories" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_categories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_categories" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_faq" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_faq" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_faq" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_faq" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_faq" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "question", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_faq" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "answer", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_faq" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_related_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_related_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_related_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_related_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "seoTitle", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "seoDescription", + "entityType": "columns", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "code", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "excerpt", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "views", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "featured", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "noIndex", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "author", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "animation", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "category", + "entityType": "columns", + "schema": "public", + "table": "example_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "example_articles_gallery" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "relatedItemId", + "entityType": "columns", + "schema": "public", + "table": "example_articles_gallery" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "position", + "entityType": "columns", + "schema": "public", + "table": "example_articles_gallery" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_articles_gallery" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "example_categories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_categories" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_categories" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "example_categories" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "featured", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "itemId", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "languageId", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "1", + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "body", + "entityType": "columns", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "type": "serial", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "example_pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "createdAt", + "entityType": "columns", + "schema": "public", + "table": "example_pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updatedAt", + "entityType": "columns", + "schema": "public", + "table": "example_pages" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "publishedAt", + "entityType": "columns", + "schema": "public", + "table": "example_pages" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'draft'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "example_pages" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "example_pages" + }, + { + "type": "varchar(160)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "example_pages" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'[]'", + "generated": null, + "identity": null, + "name": "content", + "entityType": "columns", + "schema": "public", + "table": "example_pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "roleId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_permissions_role_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_permissions_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_permissions_updated_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "token", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_sessions_token_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_sessions_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "revisionId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "fileId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_file_refs_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "fileId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_file_refs_file_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "version", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"languageId\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_revisions_item_version_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "version", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"languageId\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_revisions_translation_version_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "version", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_revisions_language_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_revisions_plugin_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "actorUserId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_revisions_actor_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "action", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "status = 'pending'", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_schedules_active_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "scheduledFor", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_schedules_due_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_schedules_item_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_schedules_plugin_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdBy", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_schedules_created_by_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"languageId\" IS NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_slug_history_shared_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": "\"languageId\" IS NOT NULL", + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_slug_history_locale_unique", + "entityType": "indexes", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "contentTypeId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_slug_history_item_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "pluginId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_content_slug_history_plugin_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_content_slug_history" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "lastRun", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_cron_last_run_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_cron" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_admin_dashboard_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_files_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_files_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_files" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "code", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_languages_code_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_languages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_languages_name_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_languages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageCode", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_languages_words_lang_code_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_languages_words" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_logs_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_logs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "roleId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderators_permissions_role_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderators_permissions_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_moderators_permissions_updated_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_navigation_position_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_navigation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "location", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_navigation_location_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_navigation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "parentId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_navigation_parent_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_navigation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "expiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_passkey_challenges_expires_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_passkey_challenges_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_passkeys_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "webauthnUserId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_passkeys_webauthn_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "availableAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_queue_status_available_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_queue" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_queue_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_queue" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_roles_updated_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_roles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "search_vector", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "core_search_index_search_vector_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_index_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "authorIds", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "gin", + "concurrently": false, + "name": "core_search_index_author_ids_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemType", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_index_item_type_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageCode", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_index_language_code_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "isPublic", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_index_is_public_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_sessions_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_sessions" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "ipAddress", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_sessions_known_devices_ip_address_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_sessions_known_devices" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "nameCode", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_name_code_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_name_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_email_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "avatarId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_avatar_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "coverId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_cover_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_created_at_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_secondary_roles_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "roleId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_secondary_roles_role_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_sso_user_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_sso" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "userId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "providerId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_sso_operations_user_provider_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "expiresAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_users_sso_operations_expires_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_categories_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_categories_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_categories_translations_language_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_status_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "coverImage", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_cover_image_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_author_id_position_key", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_author_id_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_category_id_position_key", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_category_id_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_translations_language_id_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "friendlyUrl", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "blog_posts_translations_language_id_friendly_url_key", + "entityType": "indexes", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "syndicationPriority", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_syndication_priority_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_categories_position_key", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_categories_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_faq_position_key", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles_faq" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_related_articles_position_key", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles_related_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_related_articles_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles_related_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_translations_language_id_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_advanced_articles_translations_language_id_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_status_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "code", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_code_key", + "entityType": "indexes", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "author", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_author_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "animation", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_animation_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "category", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_category_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "itemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "position", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_gallery_position_key", + "entityType": "indexes", + "schema": "public", + "table": "example_articles_gallery" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "relatedItemId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_articles_gallery_related_item_id_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_articles_gallery" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_categories_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_categories_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_categories" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_localized_articles_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_localized_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_localized_articles_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_localized_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_localized_articles_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_localized_articles" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_localized_articles_translations_language_id_status_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "languageId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_localized_articles_translations_language_id_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_pages_slug_key", + "entityType": "indexes", + "schema": "public", + "table": "example_pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "createdAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_pages_created_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "updatedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_pages_updated_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_pages" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "status", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "publishedAt", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "example_pages_status_published_at_idx", + "entityType": "indexes", + "schema": "public", + "table": "example_pages" + }, + { + "nameExplicit": false, + "columns": [ + "roleId" + ], + "schemaTo": "public", + "tableTo": "core_roles", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_admin_permissions_roleId_core_roles_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_admin_permissions_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_admin_permissions" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_admin_sessions_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "nameExplicit": false, + "columns": [ + "deviceId" + ], + "schemaTo": "public", + "tableTo": "core_sessions_known_devices", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_admin_sessions_deviceId_core_sessions_known_devices_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_admin_sessions" + }, + { + "nameExplicit": false, + "columns": [ + "revisionId" + ], + "schemaTo": "public", + "tableTo": "core_content_revisions", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "core_content_file_refs_5get6SfhBPHr_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "nameExplicit": false, + "columns": [ + "fileId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "core_content_file_refs_fileId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_content_file_refs" + }, + { + "nameExplicit": false, + "columns": [ + "actorUserId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "core_content_revisions_actorUserId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_content_revisions" + }, + { + "nameExplicit": false, + "columns": [ + "createdBy" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "core_content_schedules_createdBy_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_content_schedules" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_admin_dashboard_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_admin_dashboard" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_files_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_files" + }, + { + "nameExplicit": false, + "columns": [ + "languageCode" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "code" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_languages_words_languageCode_core_languages_code_fk", + "entityType": "fks", + "schema": "public", + "table": "core_languages_words" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "core_logs_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_logs" + }, + { + "nameExplicit": false, + "columns": [ + "roleId" + ], + "schemaTo": "public", + "tableTo": "core_roles", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_moderators_permissions_roleId_core_roles_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_moderators_permissions_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_moderators_permissions" + }, + { + "nameExplicit": false, + "columns": [ + "parentId" + ], + "schemaTo": "public", + "tableTo": "core_navigation", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_navigation_parentId_core_navigation_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_navigation" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_passkey_challenges_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_passkey_challenges" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_passkeys_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_passkeys" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_sessions_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_sessions" + }, + { + "nameExplicit": false, + "columns": [ + "deviceId" + ], + "schemaTo": "public", + "tableTo": "core_sessions_known_devices", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_sessions_deviceId_core_sessions_known_devices_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_sessions" + }, + { + "nameExplicit": false, + "columns": [ + "roleId" + ], + "schemaTo": "public", + "tableTo": "core_roles", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "name": "core_users_roleId_core_roles_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": false, + "columns": [ + "language" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "code" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET DEFAULT", + "name": "core_users_language_core_languages_code_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": false, + "columns": [ + "avatarId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_users_avatarId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": false, + "columns": [ + "coverId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_users_coverId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_confirm_emails_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users_confirm_emails" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_forgot_password_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users_forgot_password" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_secondary_roles_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "nameExplicit": false, + "columns": [ + "roleId" + ], + "schemaTo": "public", + "tableTo": "core_roles", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_secondary_roles_roleId_core_roles_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_sso_userId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_users_sso" + }, + { + "nameExplicit": false, + "columns": [ + "avatarFileId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "name": "core_users_sso_avatarFileId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_sso" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_sso_operations_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_sso_profile_sources_userId_core_users_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "nameExplicit": true, + "columns": [ + "userId", + "providerId" + ], + "schemaTo": "public", + "tableTo": "core_users_sso", + "columnsTo": [ + "userId", + "providerId" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "core_users_sso_profile_sources_connection_fkey", + "entityType": "fks", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "blog_categories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "blog_categories_translations_itemId_blog_categories_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "nameExplicit": false, + "columns": [ + "languageId" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "blog_categories_translations_languageId_core_languages_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "nameExplicit": false, + "columns": [ + "coverImage" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "blog_posts_coverImage_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "blog_posts" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "blog_posts", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "blog_posts_author_id_itemId_blog_posts_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "blog_posts_author_id_relatedItemId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "blog_posts", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "blog_posts_category_id_itemId_blog_posts_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "blog_categories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "blog_posts_category_id_relatedItemId_blog_categories_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "blog_posts", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "blog_posts_translations_itemId_blog_posts_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "nameExplicit": false, + "columns": [ + "languageId" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "blog_posts_translations_languageId_core_languages_id_fk", + "entityType": "fks", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "example_advanced_articles", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_advanced_articles_categories_itemId_example_advanced_ar", + "entityType": "fks", + "schema": "public", + "table": "example_advanced_articles_categories" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "example_categories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "example_advanced_articles_categories_relatedItemId_example_cate", + "entityType": "fks", + "schema": "public", + "table": "example_advanced_articles_categories" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "example_advanced_articles", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_advanced_articles_faq_itemId_example_advanced_articles_", + "entityType": "fks", + "schema": "public", + "table": "example_advanced_articles_faq" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "example_advanced_articles", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_advanced_articles_related_articles_itemId_example_advan", + "entityType": "fks", + "schema": "public", + "table": "example_advanced_articles_related_articles" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "example_advanced_articles", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_advanced_articles_related_articles_relatedItemId_exampl", + "entityType": "fks", + "schema": "public", + "table": "example_advanced_articles_related_articles" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "example_advanced_articles", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_advanced_articles_translations_itemId_example_advanced_", + "entityType": "fks", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "nameExplicit": false, + "columns": [ + "languageId" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "example_advanced_articles_translations_languageId_core_language", + "entityType": "fks", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "nameExplicit": false, + "columns": [ + "author" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "example_articles_author_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": false, + "columns": [ + "animation" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "example_articles_animation_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": false, + "columns": [ + "category" + ], + "schemaTo": "public", + "tableTo": "example_categories", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "example_articles_category_example_categories_id_fk", + "entityType": "fks", + "schema": "public", + "table": "example_articles" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "example_articles", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_articles_gallery_itemId_example_articles_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_articles_gallery" + }, + { + "nameExplicit": false, + "columns": [ + "relatedItemId" + ], + "schemaTo": "public", + "tableTo": "core_files", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "example_articles_gallery_relatedItemId_core_files_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "example_articles_gallery" + }, + { + "nameExplicit": false, + "columns": [ + "itemId" + ], + "schemaTo": "public", + "tableTo": "example_localized_articles", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "CASCADE", + "name": "example_localized_articles_translations_itemId_example_localize", + "entityType": "fks", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "nameExplicit": false, + "columns": [ + "languageId" + ], + "schemaTo": "public", + "tableTo": "core_languages", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "RESTRICT", + "name": "example_localized_articles_translations_languageId_core_languag", + "entityType": "fks", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "columns": [ + "userId", + "roleId" + ], + "nameExplicit": false, + "name": "core_users_secondary_roles_userId_roleId_pk", + "entityType": "pks", + "schema": "public", + "table": "core_users_secondary_roles" + }, + { + "columns": [ + "userId", + "field" + ], + "nameExplicit": false, + "name": "core_users_sso_profile_sources_pkey", + "entityType": "pks", + "schema": "public", + "table": "core_users_sso_profile_sources" + }, + { + "columns": [ + "itemId", + "languageId" + ], + "nameExplicit": true, + "name": "blog_categories_translations_item_id_language_id_pk", + "entityType": "pks", + "schema": "public", + "table": "blog_categories_translations" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "blog_posts_author_id_pk", + "entityType": "pks", + "schema": "public", + "table": "blog_posts_author_id" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "blog_posts_category_id_pk", + "entityType": "pks", + "schema": "public", + "table": "blog_posts_category_id" + }, + { + "columns": [ + "itemId", + "languageId" + ], + "nameExplicit": true, + "name": "blog_posts_translations_item_id_language_id_pk", + "entityType": "pks", + "schema": "public", + "table": "blog_posts_translations" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "example_advanced_articles_categories_pk", + "entityType": "pks", + "schema": "public", + "table": "example_advanced_articles_categories" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "example_advanced_articles_related_articles_pk", + "entityType": "pks", + "schema": "public", + "table": "example_advanced_articles_related_articles" + }, + { + "columns": [ + "itemId", + "languageId" + ], + "nameExplicit": true, + "name": "example_advanced_articles_translations_item_id_language_id_pk", + "entityType": "pks", + "schema": "public", + "table": "example_advanced_articles_translations" + }, + { + "columns": [ + "itemId", + "relatedItemId" + ], + "nameExplicit": true, + "name": "example_articles_gallery_pk", + "entityType": "pks", + "schema": "public", + "table": "example_articles_gallery" + }, + { + "columns": [ + "itemId", + "languageId" + ], + "nameExplicit": true, + "name": "example_localized_articles_translations_item_id_language_id_pk", + "entityType": "pks", + "schema": "public", + "table": "example_localized_articles_translations" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_admin_permissions_pkey", + "schema": "public", + "table": "core_admin_permissions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_admin_sessions_pkey", + "schema": "public", + "table": "core_admin_sessions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_file_refs_pkey", + "schema": "public", + "table": "core_content_file_refs", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_revisions_pkey", + "schema": "public", + "table": "core_content_revisions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_schedules_pkey", + "schema": "public", + "table": "core_content_schedules", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_content_slug_history_pkey", + "schema": "public", + "table": "core_content_slug_history", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_cron_pkey", + "schema": "public", + "table": "core_cron", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_admin_dashboard_pkey", + "schema": "public", + "table": "core_admin_dashboard", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_files_pkey", + "schema": "public", + "table": "core_files", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_languages_pkey", + "schema": "public", + "table": "core_languages", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_languages_words_pkey", + "schema": "public", + "table": "core_languages_words", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_logs_pkey", + "schema": "public", + "table": "core_logs", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_moderators_permissions_pkey", + "schema": "public", + "table": "core_moderators_permissions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_navigation_pkey", + "schema": "public", + "table": "core_navigation", + "entityType": "pks" + }, + { + "columns": [ + "pageId" + ], + "nameExplicit": false, + "name": "core_page_layouts_pkey", + "schema": "public", + "table": "core_page_layouts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_passkey_challenges_pkey", + "schema": "public", + "table": "core_users_passkey_challenges", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_passkeys_pkey", + "schema": "public", + "table": "core_users_passkeys", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_queue_pkey", + "schema": "public", + "table": "core_queue", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_roles_pkey", + "schema": "public", + "table": "core_roles", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_search_index_pkey", + "schema": "public", + "table": "core_search_index", + "entityType": "pks" + }, + { + "columns": [ + "name" + ], + "nameExplicit": false, + "name": "core_secrets_pkey", + "schema": "public", + "table": "core_secrets", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_sessions_pkey", + "schema": "public", + "table": "core_sessions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_sessions_known_devices_pkey", + "schema": "public", + "table": "core_sessions_known_devices", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_pkey", + "schema": "public", + "table": "core_users", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_confirm_emails_pkey", + "schema": "public", + "table": "core_users_confirm_emails", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_forgot_password_pkey", + "schema": "public", + "table": "core_users_forgot_password", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "core_users_sso_operations_pkey", + "schema": "public", + "table": "core_users_sso_operations", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "blog_categories_pkey", + "schema": "public", + "table": "blog_categories", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "blog_posts_pkey", + "schema": "public", + "table": "blog_posts", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "example_advanced_articles_pkey", + "schema": "public", + "table": "example_advanced_articles", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "example_advanced_articles_faq_pkey", + "schema": "public", + "table": "example_advanced_articles_faq", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "example_articles_pkey", + "schema": "public", + "table": "example_articles", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "example_categories_pkey", + "schema": "public", + "table": "example_categories", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "example_localized_articles_pkey", + "schema": "public", + "table": "example_localized_articles", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "example_pages_pkey", + "schema": "public", + "table": "example_pages", + "entityType": "pks" + }, + { + "nameExplicit": true, + "columns": [ + "itemType", + "itemId", + "languageCode" + ], + "nullsNotDistinct": false, + "name": "core_search_index_item_unique", + "entityType": "uniques", + "schema": "public", + "table": "core_search_index" + }, + { + "nameExplicit": true, + "columns": [ + "providerId", + "providerAccountId" + ], + "nullsNotDistinct": false, + "name": "core_users_sso_provider_account_key", + "entityType": "uniques", + "schema": "public", + "table": "core_users_sso" + }, + { + "nameExplicit": true, + "columns": [ + "userId", + "providerId" + ], + "nullsNotDistinct": false, + "name": "core_users_sso_user_provider_key", + "entityType": "uniques", + "schema": "public", + "table": "core_users_sso" + }, + { + "nameExplicit": false, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "core_admin_sessions_token_unique", + "schema": "public", + "table": "core_admin_sessions", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "nullsNotDistinct": false, + "name": "core_admin_dashboard_userId_unique", + "schema": "public", + "table": "core_admin_dashboard", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "key" + ], + "nullsNotDistinct": false, + "name": "core_files_key_unique", + "schema": "public", + "table": "core_files", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "code" + ], + "nullsNotDistinct": false, + "name": "core_languages_code_unique", + "schema": "public", + "table": "core_languages", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "tokenHash" + ], + "nullsNotDistinct": false, + "name": "core_users_passkey_challenges_tokenHash_key", + "schema": "public", + "table": "core_users_passkey_challenges", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "credentialId" + ], + "nullsNotDistinct": false, + "name": "core_users_passkeys_credentialId_key", + "schema": "public", + "table": "core_users_passkeys", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "core_sessions_token_unique", + "schema": "public", + "table": "core_sessions", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "publicId" + ], + "nullsNotDistinct": false, + "name": "core_sessions_known_devices_publicId_unique", + "schema": "public", + "table": "core_sessions_known_devices", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "nameCode" + ], + "nullsNotDistinct": false, + "name": "core_users_nameCode_unique", + "schema": "public", + "table": "core_users", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "name" + ], + "nullsNotDistinct": false, + "name": "core_users_name_unique", + "schema": "public", + "table": "core_users", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "email" + ], + "nullsNotDistinct": false, + "name": "core_users_email_unique", + "schema": "public", + "table": "core_users", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "core_users_confirm_emails_token_unique", + "schema": "public", + "table": "core_users_confirm_emails", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "userId" + ], + "nullsNotDistinct": false, + "name": "core_users_forgot_password_userId_unique", + "schema": "public", + "table": "core_users_forgot_password", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "token" + ], + "nullsNotDistinct": false, + "name": "core_users_forgot_password_token_unique", + "schema": "public", + "table": "core_users_forgot_password", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": [ + "tokenHash" + ], + "nullsNotDistinct": false, + "name": "core_users_sso_operations_tokenHash_key", + "schema": "public", + "table": "core_users_sso_operations", + "entityType": "uniques" + }, + { + "value": "\"intent\" IN ('link', 'import', 'sync', 'preview')", + "name": "core_users_sso_operations_intent_check", + "entityType": "checks", + "schema": "public", + "table": "core_users_sso_operations" + }, + { + "value": "\"field\" IN ('avatar', 'firstName', 'lastName')", + "name": "core_users_sso_profile_sources_field_check", + "entityType": "checks", + "schema": "public", + "table": "core_users_sso_profile_sources" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/apps/web/content/docs/dev/events/built-in-events.mdx b/apps/web/content/docs/dev/events/built-in-events.mdx index 06fff643a..bfa0b295f 100644 --- a/apps/web/content/docs/dev/events/built-in-events.mdx +++ b/apps/web/content/docs/dev/events/built-in-events.mdx @@ -38,7 +38,7 @@ core event as for one of your own. ## Core events (`@vitnode/core`) -Twelve names, all declared in `VitNodeEvents` in +Fourteen names, all declared in `VitNodeEvents` in `packages/vitnode/src/api/models/events.ts`. Every one of them fires **after** the write it describes has committed. @@ -47,7 +47,9 @@ the write it describes has committed. | `user.created` | `{ userId, email, name, emailVerified }` | A user row is inserted - public sign-up, AdminCP creation, or SSO sign-up | | `user.updated` | `{ userId, email, name }` | A user is edited in the AdminCP (profile fields and/or role assignments) | | `user.deleted` | `{ userId, email }` | Never - the name is declared for plugins, core has no deletion flow | -| `user.sso.linked` | `{ userId, email, providerId }` | A visitor proves an existing account is theirs with its password and an SSO identity is linked to it | +| `user.sso.linked` | `{ userId, email, providerId }` | An SSO identity is linked to an existing account - at login with its password, or from **Settings → Connected accounts** | +| `user.sso.unlinked` | `{ userId, providerId }` | A member disconnects a provider in **Settings → Connected accounts** | +| `user.sso.profile_synced` | `{ userId, providerId, fields, trigger }` | Profile fields were copied from a provider - by **Sync now**, **Import** or an opted-in sign-in | | `user.passkey.created` | `{ userId, passkeyId }` | A member adds a [passkey](/docs/dev/passkeys) in **Settings → Security** | | `user.passkey.updated` | `{ userId, passkeyId, name }` | A member renames one of their passkeys | | `user.passkey.deleted` | `{ userId, passkeyId }` | A member removes one of their passkeys | @@ -178,10 +180,16 @@ same two triggers. -Emitted by `SSOModel.link` after the `core_users_sso` row is committed - the -moment a social identity that arrived with an already-registered email is tied -to that account. It does not fire on the first sign-in through a provider (that -is a `user.created`) or on a later sign-in through an identity that is already +Emitted after the `core_users_sso` row is committed, from two places: + +- `SSOModel.link` - a social identity that arrived with an already-registered + email is tied to that account once its password checks out; +- `SsoConnectionModel.callback` - a signed-in member connects a provider from + [Connected accounts](/docs/dev/sso/connected-accounts). The provider's email + may differ from the account's; `email` is always the VitNode account's. + +It does not fire on the first sign-in through a provider (that is a +`user.created`) or on a later sign-in through an identity that is already linked. + + +Emitted by `SsoConnectionModel.disconnect` after the connection and its profile +sync settings are deleted. Nothing is revoked at the provider - the member still +has to remove the app there if they want to. + + + +**A listener would** email the member that a sign-in method was removed. + + + + +Emitted when at least one profile field actually changed because of a provider. +Fields that were missing, not allowed or already equal are left out, and an +avatar that failed to download never counts. A name change also emits +`user.updated`, and an avatar change emits `user.avatar.updated`. + + + +**A listener would** refresh a search index or a cached member card. + diff --git a/apps/web/content/docs/dev/sso/connected-accounts.mdx b/apps/web/content/docs/dev/sso/connected-accounts.mdx new file mode 100644 index 000000000..81806f365 --- /dev/null +++ b/apps/web/content/docs/dev/sso/connected-accounts.mdx @@ -0,0 +1,132 @@ +--- +title: Connected Accounts +description: Let members connect Google, Discord, Facebook or custom SSO accounts from Settings, import their avatar and name, and keep them in sync on sign-in. +icon: Link +--- + +import { TypeTable } from "fumadocs-ui/components/type-table" + +**Settings → Connected accounts** (`/settings/sso`) is where a signed-in member manages their SSO accounts. It shows up in the settings menu as soon as at least one [SSO adapter](/docs/dev/sso) is configured - no extra setup, no extra redirect URIs. + +// Image prompt: A settings page titled "Connected accounts" with three grouped lists - "Sign-in accounts" (Google with an email and "Connected Mar 14", Discord, Facebook with a Connect button), "Profile" (Avatar, First name, Last name rows, each with its current value and a "From Google" / "Manual" dropdown) and "Sync" (Google · First name, an "On sign-in" switch and a "Sync now" button), light theme, blue accents. + +The page uses the same grouped rows as the rest of Settings, in three groups: + +- **Sign-in accounts** - every configured provider with the connected account and date. **Connect**, **Import avatar and name** (a one-time copy after a preview, labelled with what the provider shares) and **Disconnect** live here. +- **Profile** - shown once a connected account can supply something. One row per field it can supply: the value the member has now, and a dropdown choosing where it comes from (**Manual**, or a connected account that can supply it). Changes save immediately. +- **Sync** - shown once a field follows an account. One row per account with the fields it covers, an **On sign-in** switch and **Sync now**, which confirms it's the member at the provider and updates those fields right away. They stay signed in; no sign-out, no new session. + +Only the avatar, first name and last name are ever imported. Email, username, display name, language, birthday, phone and headline are never touched. A provider's email is shown as connection info and nothing more. + +## How connecting works + +1. The member selects **Connect**. `POST /users/sso/connections/{providerId}/authorize` stores a short-lived, single-use **state** bound to that member, that provider and the `link` intent - only its SHA-256 hash is kept, in `core_users_sso_operations`. +2. The browser goes to the provider and comes back to the usual callback, `/login/sso/{providerId}`. That page recognises a settings state and calls the authenticated `POST /users/sso/connections/{providerId}/callback` instead of the public sign-in. +3. The state is spent the moment it is looked up - even a failed attempt burns it - and it must still be fresh (ten minutes), for the same provider, the same intent and the **currently signed-in member**. Anything else is a `400 invalid_state`. +4. The provider account is linked to the member. Accounts are never merged by email. +5. The member lands back on `/settings/sso` with a toast. + +A provider account already linked to someone else answers `409 account_taken`. A member can have **one account per provider**; a second one answers `409 provider_already_connected`. Both rules are unique constraints on `core_users_sso`, so two racing requests can't sneak past them. + +Connecting never creates a session - public or AdminCP - and never changes the profile. + + + The public sign-in and the login-time "Connect *Provider* to your account" + password flow work exactly as before. See [How Accounts Link](/docs/dev/sso#how-accounts-link). + + +## Disconnecting + +`DELETE /users/sso/connections/{providerId}` removes the connection and its sync settings (the profile sources cascade). Values already imported stay on the profile. + +It refuses with `409 last_sign_in_method` when nothing else could sign the member in. What counts: + +| Method | Counts when | +| ------------------ | ------------------------------------------------------------------ | +| Password | password sign-in is enabled **and** the member has a password | +| Passkeys | passkeys are enabled **and** the member has at least one | +| Other SSO accounts | the provider is still configured in `ssoAdapters` | + +The check and the delete run in one transaction that locks the member's row - the same lock passkey removal takes - so two parallel requests can't remove the last two methods at once. + +Disconnecting doesn't revoke anything at the provider. The page says so, and members remove the app in their provider account themselves. + +## Import now + +1. The member picks fields the adapter supports and selects **Continue**. The state now also carries the `import` intent and the chosen fields. +2. After the provider round trip, VitNode checks it's the **same provider account** that is connected (`409 account_mismatch` otherwise) and stores a ten-minute preview of just those fields. +3. `/settings/sso?import={providerId}` shows current values next to the new ones. The member confirms a subset, and `POST .../{providerId}/import` applies it. The preview works once. + +A fresh round trip every time means no provider tokens are ever stored. + +## Sync now + +Each field - `avatar`, `firstName`, `lastName` - has at most one source (`core_users_sso_profile_sources`, primary key `userId + field`). Different fields can use different providers. Picking a source in a field's dropdown moves the field to that account. + +**Sync now** starts a round trip with the `sync` intent. The server - not the browser - decides which fields it covers: the ones sourced from that provider when the round trip starts (`400 nothing_to_sync` if there are none). On the way back it checks it's the same provider account (`409 account_mismatch`), re-reads the sources so a field made manual in the meantime is skipped, applies the values and answers with a per-field result. The member stays on `/settings/sso` with a toast, still signed in. No token is stored, and no session is created. + +## Sync on sign-in + +When the **On sign-in** switch is on, every sign-in through that provider refreshes the same fields automatically. It's optional - Sync now works without it. There's no polling and no background job. + +The rules that keep this predictable: + +- Connecting an account doesn't copy anything. Existing connections start as manual with sign-in updates off. +- A missing or empty value from the provider never erases a local one. +- Sync now and sign-in updates only ever touch fields sourced from that provider. +- Editing a field yourself - `PATCH /users/me`, an avatar upload or removal, or staff changing the avatar in the AdminCP - switches that field back to manual. +- Importing a field that syncs from a different provider switches it to manual too, and the response lists it in `manualFields`. +- Role limits still apply: personal information rules for names, avatar upload permission and size for the avatar. Importing names never turns on `showRealName`. +- Anything that goes wrong during a sign-in sync is swallowed - the member is still signed in. + +## Avatars + +Imported avatars go through the normal avatar pipeline (`setUserImage`), so storage, image processing, file ownership and cleanup of the previous avatar are the same as an upload. The download (`downloadRemoteImage`) only speaks HTTPS, resolves the host and refuses private, loopback, link-local and reserved addresses - again at connect time, and again after every redirect (three at most) - caps the size at the role's limit (5 MB hard cap), and trusts the bytes, not the `Content-Type`: only real JPEG, PNG or WebP files under 8192 × 8192 pass. + +The URL and a SHA-256 of the last imported picture are kept on the connection, so the same avatar isn't downloaded again while it's still shown, or stored twice when a provider hands out a new URL for the same image. A failed download leaves the current avatar alone. + +## API + +All routes live under `users/sso/connections`, need a signed-in member, and use the public session only. + +| Method | Path | Does | +| -------- | ------------------------- | ----------------------------------------- | +| `GET` | `/` | Providers, connections, sources, sign-in summary | +| `PUT` | `/preferences` | Field sources and sync toggles | +| `POST` | `/{providerId}/authorize` | Starts a `link`, `import` or `sync` round trip | +| `POST` | `/{providerId}/callback` | Finishes it (`sync` answers with `results`) | +| `GET` | `/{providerId}/import` | The pending import preview | +| `POST` | `/{providerId}/import` | Applies selected preview fields | +| `DELETE` | `/{providerId}/import` | Discards the preview | +| `DELETE` | `/{providerId}` | Disconnects | + +```ts title="Calling it from your own UI" +import { fetcher } from "@vitnode/core/tanstack/fetcher" + +const res = await fetcher({ + plugin: "@vitnode/core", + method: "get", + module: "users/sso/connections", + path: "/", +}) +``` + +Mutations take JSON bodies, which the browser can't send cross-site without a CORS preflight, on top of the API's CSRF middleware. Provider tokens never leave the server and never reach logs or events. + +## Events + +- [`user.sso.linked`](/docs/dev/events/built-in-events) - a provider was connected. +- [`user.sso.unlinked`](/docs/dev/events/built-in-events) - a provider was disconnected. +- [`user.sso.profile_synced`](/docs/dev/events/built-in-events) - profile fields changed because of a provider. + +## Adapter support + +Which fields a provider can supply comes from the adapter's `profileFields`. The page tells "this provider can't provide it" apart from "this provider didn't send it this time". + +| Adapter | Avatar | First name | Last name | +| -------- | ------ | ---------- | --------- | +| Google | ✓ | ✓ | ✓ | +| Discord | ✓ | - | - | +| Facebook | ✓ | ✓ | ✓ | + +Discord has no real-name fields, and VitNode never guesses a first and last name from a username or a full name. For your own provider, see [Custom SSO Adapter](/docs/dev/sso/custom-adapter#profile-data). diff --git a/apps/web/content/docs/dev/sso/custom-adapter.mdx b/apps/web/content/docs/dev/sso/custom-adapter.mdx index 3fca8d39f..6d351714c 100644 --- a/apps/web/content/docs/dev/sso/custom-adapter.mdx +++ b/apps/web/content/docs/dev/sso/custom-adapter.mdx @@ -31,6 +31,7 @@ export const GitHubSSOApiPlugin = ({ return { id, name: "GitHub", + profileFields: ["avatar"], // 1. Build Authorization URL getUrl: ({ state }) => `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=read:user,user:email&state=${state}`, @@ -135,15 +136,47 @@ A **GitHub** login button automatically renders on `/login` and `/register`, and type: "(code: string) => Promise<{ access_token: string; token_type: string }>", }, fetchUser: { - description: "Fetches user profile (id, email, username, avatarUrl). Return only an email the provider has verified - VitNode treats it as confirmed and will mark a matching unconfirmed account verified. The built-in adapters reject unverified addresses with a 400.", + description: "Fetches the user profile: id, email, username, and optionally avatarUrl, firstName and lastName. Return only an email the provider has verified - VitNode treats it as confirmed and will mark a matching unconfirmed account verified. The built-in adapters reject unverified addresses with a 400.", required: true, - type: "(token: TokenResponse) => Promise", + type: "(token: TokenResponse) => Promise", + }, + profileFields: { + description: "Which profile fields the provider can supply for Connected accounts: 'avatar', 'firstName', 'lastName'. Leave it out and nothing is ever imported from this adapter.", + required: false, + type: "readonly ('avatar' | 'firstName' | 'lastName')[]", }, }} /> See [Provider Icons](/docs/dev/sso/icons) for the accepted `icon` formats, and for registering a React component instead. +## Profile data + +[Connected accounts](/docs/dev/sso/connected-accounts) can import a member's avatar, first name and last name from your provider. Two steps: + +1. Declare what the provider can supply in `profileFields`. +2. Return those values from `fetchUser` as `avatarUrl`, `firstName` and `lastName`. + +```ts +fetchUser: async ({ access_token, token_type }) => { + // ... + return { + id: String(user.id), + email: primaryEmail, + username: user.login, + avatarUrl: user.avatar_url ?? null, + } +}, +profileFields: ["avatar"], +``` + +All three are optional, so adapters written before this still work - they just have nothing to import. A few rules keep imports honest: + +- Return `null` (or leave it out) when the provider has no value. VitNode never erases a member's value with an empty one. +- Only send separate first and last names the provider actually has. Don't split a full name - `"Mary Ann Smith"` has no safe answer. +- `avatarUrl` must be `https`. VitNode downloads and re-stores it itself, with SSRF protection and size and type checks, so you don't have to. +- Don't add scopes just for profile data. If a field needs extra permission, leave it out of `profileFields`. + ## Built-in SSO Adapters diff --git a/apps/web/content/docs/dev/sso/discord.mdx b/apps/web/content/docs/dev/sso/discord.mdx index 3a28c33e5..8669f2b12 100644 --- a/apps/web/content/docs/dev/sso/discord.mdx +++ b/apps/web/content/docs/dev/sso/discord.mdx @@ -195,11 +195,18 @@ permissions. | Profile URL | `https://discord.com/api/users/@me` | | Scopes | `identify email` | | Provider id | `discord` | -| Fields read | `id`, `email`, `username` | +| Fields read | `id`, `email`, `username`, `avatar` | Both scopes are required: `identify` for the id and username, `email` for the address the account is created with. +The `avatar` hash becomes +`https://cdn.discordapp.com/avatars/{id}/{avatar}.png?size=512`, which +[Connected accounts](/docs/dev/sso/connected-accounts) can import. A member +still on Discord's default avatar has none. Discord has no first or last name, +and VitNode doesn't guess one from `username` or `global_name`, so only the +avatar is offered. + ## Gotchas diff --git a/apps/web/content/docs/dev/sso/facebook.mdx b/apps/web/content/docs/dev/sso/facebook.mdx index d32690b0f..fb4d39874 100644 --- a/apps/web/content/docs/dev/sso/facebook.mdx +++ b/apps/web/content/docs/dev/sso/facebook.mdx @@ -242,17 +242,22 @@ permissions than you expected. | Setting | Value | | ------------- | ---------------------------------------------------------- | -| Authorize URL | `https://www.facebook.com/v22.0/dialog/oauth` | -| Token URL | `https://graph.facebook.com/v22.0/oauth/access_token` | -| Profile URL | `https://graph.facebook.com/v22.0/me?fields=id,name,email` | +| Authorize URL | `https://www.facebook.com/v26.0/dialog/oauth` | +| Token URL | `https://graph.facebook.com/v26.0/oauth/access_token` | +| Profile URL | `https://graph.facebook.com/v26.0/me` | | Scopes | `public_profile,email` | | Provider id | `facebook` | -| Fields read | `id`, `name`, `email` | +| Fields read | `id`, `name`, `email`, `first_name`, `last_name`, `picture.width(512).height(512){url,is_silhouette}` | The account name VitNode creates comes from Facebook's `name` field, stripped of characters that cannot appear in a profile URL. The Graph API version is pinned -to `v22.0` in the adapter, so a Meta version bump never changes what this -install sends. +to `v26.0` in the adapter (`FACEBOOK_GRAPH_VERSION`), so a Meta version bump +never changes what this install sends. + +`first_name`, `last_name` and `picture` are default public profile fields, so +[Connected accounts](/docs/dev/sso/connected-accounts) can import them without +asking for anything beyond `public_profile`. The default silhouette picture +counts as no avatar. ## Gotchas diff --git a/apps/web/content/docs/dev/sso/google.mdx b/apps/web/content/docs/dev/sso/google.mdx index bca939655..d3ca1a0a7 100644 --- a/apps/web/content/docs/dev/sso/google.mdx +++ b/apps/web/content/docs/dev/sso/google.mdx @@ -244,11 +244,16 @@ permissions than you expected. | Profile URL | `https://www.googleapis.com/oauth2/v1/userinfo` | | Scopes | `openid profile email` | | Provider id | `google` | -| Fields read | `id`, `email`, `name`, `verified_email` | +| Fields read | `id`, `email`, `name`, `verified_email`, `given_name`, `family_name`, `picture` | The account name VitNode creates comes from Google's `name` field, stripped of characters that cannot appear in a profile URL. +`given_name`, `family_name` and `picture` come with the `profile` scope Google +already asks for, so [Connected accounts](/docs/dev/sso/connected-accounts) can +import all three with no extra permission. Google never guarantees them - an +account without one simply has nothing to import for that field. + ## Gotchas diff --git a/apps/web/content/docs/dev/sso/index.mdx b/apps/web/content/docs/dev/sso/index.mdx index 6dfe34293..94648628c 100644 --- a/apps/web/content/docs/dev/sso/index.mdx +++ b/apps/web/content/docs/dev/sso/index.mdx @@ -82,7 +82,9 @@ For example: `https://your-domain.com/login/sso/google`. When a user signs in via SSO: 1. **Known Identity**: The provider account was linked before, so the user is signed straight into it. 2. **Existing Email Match**: An account already uses that address. The visitor is asked for that account's password; once it checks out, the provider identity is linked to the account and they are signed in. From then on that provider signs them straight in (case 1). A social login can never take over an account on its own - the password is what proves ownership. -3. **New Visitor**: A new user is created with their social display name, email, and avatar. +3. **New Visitor**: A new user is created with their social display name and email. Avatar and real name stay empty until the member imports them from [Connected accounts](/docs/dev/sso/connected-accounts). + +Signed-in members can also connect more providers, import their avatar and name, and disconnect accounts in **Settings → Connected accounts** - see [Connected Accounts](/docs/dev/sso/connected-accounts). ### SSO confirms the email diff --git a/apps/web/content/docs/dev/sso/meta.json b/apps/web/content/docs/dev/sso/meta.json index d6bc40670..fbdfb4394 100644 --- a/apps/web/content/docs/dev/sso/meta.json +++ b/apps/web/content/docs/dev/sso/meta.json @@ -5,6 +5,8 @@ "pages": [ "index", "icons", + "connected-accounts", + "using-connected-accounts", "---Providers---", "google", "discord", diff --git a/apps/web/content/docs/dev/sso/using-connected-accounts.mdx b/apps/web/content/docs/dev/sso/using-connected-accounts.mdx new file mode 100644 index 000000000..113795176 --- /dev/null +++ b/apps/web/content/docs/dev/sso/using-connected-accounts.mdx @@ -0,0 +1,47 @@ +--- +title: Using connected accounts +description: A friendly guide for members - connect Google, Discord or Facebook, import your avatar and name, and disconnect safely. +icon: UserRoundCheck +--- + +Connected accounts let you sign in with an account you already have, like Google or Discord. They can also fill in your avatar and name so you don't have to - but only when you say so. + +## Connect an account + +1. Open your avatar menu, go to **Settings**, then **Connected accounts**. +2. Find the provider under **Sign-in accounts** and select **Connect**. +3. Sign in at the provider and allow access. +4. You're back in settings with "Account connected". From now on you can use that button on the login page. + +Your email here never changes, even if the provider account uses a different one. Each provider account can belong to one member only, and you can connect one account per provider. + +## Import your avatar or name + +1. Select **Import avatar and name** (or **Import avatar**, depending on what the provider shares) next to the account under **Sign-in accounts**. +2. Pick what to bring over - avatar, first name, last name - and select **Continue**. +3. Confirm it's you at the provider. +4. Review the values, untick anything you don't want, and select **Import selected**. + +Nothing changes until that last step. If the provider didn't share something, your current value stays as it is. Importing your name doesn't switch on "Show my real name". + +## Keep it up to date + +1. Under **Profile**, pick where each field comes from - for example "From Google" for your avatar. It saves right away. +2. Under **Sync**, select **Sync now** next to that account. You confirm it's you at the provider and land right back in settings with your profile updated. You stay signed in the whole time. +3. Want it automatic? Turn on **On sign-in** for that account. + +Each field follows one account at a time, and different fields can follow different accounts. + +Changed your mind and edited your name or avatar yourself? That field switches back to manual, so the next sync won't overwrite it. + +## Disconnect an account + +Select **Disconnect** next to the account under **Sign-in accounts** and confirm. You can't sign in with that account anymore, and its sync settings are gone. Your avatar and name stay as they are. + +The provider still lists this site as a connected app until you remove it in your provider account settings. + + + If that account is your only way in, you'll see "Keep a way to sign in". + Set a password with **Forgot password?** on the login page, add a passkey + under **Security**, or connect another account first. + diff --git a/apps/web/content/docs/ui/scroll-area.mdx b/apps/web/content/docs/ui/scroll-area.mdx index 25923972d..c8032cace 100644 --- a/apps/web/content/docs/ui/scroll-area.mdx +++ b/apps/web/content/docs/ui/scroll-area.mdx @@ -10,18 +10,20 @@ description: Container for scrollable content with custom scrollbars. ## Usage ```ts -import { ScrollArea } from "@vitnode/core/components/ui/scroll-area"; -import { Separator } from "@vitnode/core/components/ui/separator"; -import React from "react"; +import { ScrollArea } from '@vitnode/core/components/ui/scroll-area' +import { Separator } from '@vitnode/core/components/ui/separator' +import React from 'react' ``` ```tsx -const tags = Array.from({ length: 50 }).map((_, i, a) => `v1.2.0-beta.${a.length - i}`); +const tags = Array.from({ length: 50 }).map( + (_, i, a) => `v1.2.0-beta.${a.length - i}`, +) export function ScrollAreaDemo() { return ( -
+

Tags

{tags.map((tag) => ( @@ -31,7 +33,7 @@ export function ScrollAreaDemo() { ))}
- ); + ) } ``` diff --git a/apps/web/src/docs/examples/scroll-area.tsx b/apps/web/src/docs/examples/scroll-area.tsx index 605b0600e..f30729123 100644 --- a/apps/web/src/docs/examples/scroll-area.tsx +++ b/apps/web/src/docs/examples/scroll-area.tsx @@ -9,7 +9,7 @@ const tags = Array.from({ length: 50 }).map( export default function ScrollAreaDemo() { return ( -
+

Tags

{tags.map((tag) => ( diff --git a/apps/web/src/locales/@vitnode/core/pl.json b/apps/web/src/locales/@vitnode/core/pl.json index b9cb2b673..771ac8162 100644 --- a/apps/web/src/locales/@vitnode/core/pl.json +++ b/apps/web/src/locales/@vitnode/core/pl.json @@ -906,7 +906,8 @@ "nav": { "devices": "Urządzenia", "overview": "Przegląd", - "security": "Bezpieczeństwo" + "security": "Bezpieczeństwo", + "sso": "Połączone konta" }, "overview": { "accountTitle": "Konto", @@ -1001,7 +1002,155 @@ "title": "Klucze dostępu", "unsupported_hint": "Ta przeglądarka nie potrafi tworzyć kluczy dostępu. Użyj aktualnej wersji Chrome, Safari, Edge lub Firefox." }, - "title": "Ustawienia" + "title": "Ustawienia", + "sso": { + "desc": "Loguj się kontami, które już masz, i wybierz, które pola profilu mają aktualizować. Twój adres e-mail tutaj się nie zmienia.", + "empty": "Na tej stronie nie skonfigurowano jeszcze żadnych dostawców logowania.", + "account_unknown": "Brak udostępnionego e-maila", + "connected_at": "Połączono", + "unavailable_desc": "Nie jest już dostępne na tej stronie", + "status": { + "not_connected": "Niepołączone" + }, + "connect": { + "action": "Połącz", + "aria": "Połącz {provider}", + "success": "Konto połączone", + "success_desc": "Możesz teraz logować się przez {provider}. Twój profil się nie zmienił." + }, + "disconnect": { + "action": "Odłącz", + "title": "Odłączyć {provider}?", + "desc": "Nie zalogujesz się już tym kontem {provider}, a jego ustawienia synchronizacji zostaną usunięte. Twój awatar i imię zostają bez zmian.", + "provider_note": "To usuwa tylko połączenie tutaj. {provider} nadal pokazuje tę stronę jako połączoną aplikację, dopóki jej tam nie usuniesz.", + "confirm": "Odłącz", + "success": "Konto odłączone", + "success_desc": "{provider} nie jest już połączony z Twoim kontem." + }, + "import": { + "aria": "{action} z {provider}", + "title": "Import z {provider}", + "desc": "Wybierz, co przenieść. Potwierdzisz tożsamość w {provider}, a potem przejrzysz wartości, zanim cokolwiek się zmieni.", + "fields_legend": "Pola do importu", + "redirect_note": "Na chwilę przejdziesz do {provider} i od razu wrócisz.", + "continue": "Przejdź do {provider}", + "pick_one": "Wybierz co najmniej jedno pole do importu.", + "action_all": "Importuj awatar i imię", + "action_avatar": "Importuj awatar", + "action_name": "Importuj imię i nazwisko" + }, + "preview": { + "title": "Sprawdź import z {provider}", + "desc": "Nic się jeszcze nie zmieniło. Wybierz, których wartości użyć.", + "current": "Teraz", + "incoming": "Z {provider}", + "empty_value": "Nie ustawiono", + "missing": "{provider} tego nie udostępnił, więc Twoja wartość zostaje.", + "not_allowed": "Twoja rola nie może zmieniać tego pola.", + "sourced_elsewhere": "Obecnie aktualizuje się z {source} przy logowaniu. Import tutaj przełączy je na tryb ręczny.", + "nothing": "{provider} nie udostępnił żadnego z wybranych pól. Nic się nie zmieni.", + "apply": "Importuj zaznaczone", + "discard": "Odrzuć", + "loading": "Wczytywanie podglądu importu", + "success": "Profil zaktualizowany", + "success_desc": "Zaimportowano z {provider}: {fields}.", + "unchanged": "Wszystko już się zgadzało, więc nic się nie zmieniło.", + "avatar_failed": "Nie udało się pobrać awatara, więc zostaje obecny.", + "manual_note": "Teraz zarządzane ręcznie: {fields}." + }, + "fields": { + "avatar": "Awatar", + "avatar_desc": "Twoje zdjęcie profilowe, skopiowane na tę stronę.", + "firstName": "Imię", + "firstName_desc": "Import nie włącza opcji „Pokazuj moje prawdziwe imię”.", + "lastName": "Nazwisko", + "lastName_desc": "Import nie włącza opcji „Pokazuj moje prawdziwe imię”." + }, + "sync": { + "manual_edit_note": "Samodzielna edycja pola przełącza je na tryb ręczny, więc synchronizacja go nie nadpisze.", + "footer": "Synchronizacja potwierdzi Twoją tożsamość u dostawcy i od razu wróci tutaj. Pozostajesz zalogowany.", + "on_sign_in": "Aktualizuj też z {provider} przy logowaniu przez to konto", + "on_sign_in_short": "Przy logowaniu", + "sync_now": "Synchronizuj", + "sync_now_aria": "Synchronizuj teraz z {provider}", + "synced": "Profil zsynchronizowany", + "synced_desc": "Zaktualizowano z {provider}: {fields}.", + "up_to_date": "Wszystko już zgadza się z {provider}, więc nic się nie zmieniło." + }, + "errors": { + "access_denied": { + "title": "Połączenie anulowane", + "desc": "{provider} nie udzielił zgody, więc nic się nie zmieniło." + }, + "account_taken": { + "title": "Używane przez innego użytkownika", + "desc": "To konto {provider} jest połączone z innym kontem tutaj. Zaloguj się nim i najpierw je tam odłącz." + }, + "provider_already_connected": { + "title": "Już połączono", + "desc": "Masz już połączone konto {provider}. Odłącz je, aby użyć innego." + }, + "account_mismatch": { + "title": "To inne konto", + "desc": "Zalogowano się do innego konta {provider} niż połączone, więc nic nie zaimportowano." + }, + "invalid_state": { + "title": "To żądanie wygasło", + "desc": "Zacznij od nowa w Połączonych kontach. Każde żądanie działa raz, przez dziesięć minut." + }, + "provider_error": { + "title": "{provider} nie odpowiada", + "desc": "Nie udało się odczytać konta {provider}. Spróbuj ponownie za chwilę." + }, + "provider_not_found": { + "title": "Dostawca niedostępny", + "desc": "Ten dostawca logowania nie jest już dostępny." + }, + "not_connected": { + "title": "Niepołączone", + "desc": "To konto nie jest już połączone. Lista została odświeżona." + }, + "invalid_fields": { + "title": "Nic do zaimportowania", + "desc": "Wybierz co najmniej jedno pole, które ten dostawca udostępnia." + }, + "invalid_source": { + "title": "Nie udało się zapisać", + "desc": "Jedno z wybranych kont nie jest już połączone. Lista została odświeżona." + }, + "preview_not_found": { + "title": "Ten import wygasł", + "desc": "Zacznij go ponownie w Połączonych kontach." + }, + "unauthenticated": { + "title": "Zaloguj się ponownie", + "desc": "Sesja wygasła, zanim {provider} skończył. Zaloguj się i spróbuj ponownie." + }, + "last_sign_in_method": { + "title": "Zachowaj sposób logowania", + "desc": "To ostatni sposób wejścia na Twoje konto.", + "hint_password": "Ustaw hasło przez „Nie pamiętasz hasła?” na stronie logowania.", + "hint_passkey": "Dodaj klucz dostępu w sekcji Bezpieczeństwo.", + "hint_connect_other": "Albo najpierw połącz tutaj inne konto." + }, + "nothing_to_sync": { + "title": "Nic do synchronizacji", + "desc": "Zaznacz co najmniej jedno pole do aktualizacji z {provider}." + } + }, + "value_not_set": "Nie ustawiono", + "groups": { + "accounts": "Konta logowania", + "profile": "Profil", + "sync": "Synchronizacja" + }, + "source": { + "manual": "Ręcznie", + "from": "Z {provider}", + "saved_from": "{field}: teraz z {provider}", + "saved_manual": "{field}: teraz ręcznie" + } + } }, "sign_in": { "desc": "Witaj ponownie! Zaloguj się na swoje konto.", @@ -1141,7 +1290,10 @@ "submit": "Połącz i zaloguj się", "title": "Połącz ze swoim kontem" }, - "or": "Lub kontynuuj przez e-mail" + "or": "Lub kontynuuj przez e-mail", + "connection": { + "pending": "Kończymy z {provider}…" + } } }, "editor": { diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index e1ec2f364..1508077da 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -9,38 +9,38 @@ :root:not(.dark) { color-scheme: light; - --background: oklch(0.96 0.007 264); - --foreground: oklch(0.23 0.021 264); + --background: oklch(0.97 0 0); + --foreground: oklch(0.205 0 0); --card: oklch(1 0 0); - --card-foreground: oklch(0.23 0.021 264); + --card-foreground: oklch(0.205 0 0); --popover: oklch(1 0 0); - --popover-foreground: oklch(0.23 0.021 264); + --popover-foreground: oklch(0.205 0 0); --primary: oklch(0.52 0.2 264); --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.935 0.005 264); - --secondary-foreground: oklch(0.394 0.02 264); - --muted: oklch(0.935 0.005 264); - --muted-foreground: oklch(0.46 0.017 264); - --accent: oklch(0.92 0.008 264); - --accent-foreground: oklch(0.23 0.021 264); + --secondary: oklch(0.95 0 0); + --secondary-foreground: oklch(0.269 0 0); + --muted: oklch(0.95 0 0); + --muted-foreground: oklch(0.52 0 0); + --accent: oklch(0.95 0 0); + --accent-foreground: oklch(0.205 0 0); --destructive: oklch(0.565 0.209 27); --success: oklch(0.52 0.128 155); --warn: oklch(0.55 0.145 50); - --border: oklch(0.915 0.004 264); - --input: oklch(0.878 0.005 264); + --border: oklch(0.925 0 0); + --input: oklch(0.9 0 0); --ring: oklch(0.52 0.2 264); --chart-1: oklch(0.52 0.2 264); --chart-2: oklch(0.52 0.128 155); --chart-3: oklch(0.565 0.209 27); --chart-4: oklch(0.55 0.145 50); - --chart-5: oklch(0.565 0.017 264); - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.394 0.02 264); + --chart-5: oklch(0.6 0 0); + --sidebar: oklch(0.955 0 0); + --sidebar-foreground: oklch(0.46 0 0); --sidebar-primary: oklch(0.52 0.2 264); --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.92 0.008 264); - --sidebar-accent-foreground: oklch(0.23 0.021 264); - --sidebar-border: oklch(0.915 0.004 264); + --sidebar-accent: oklch(1 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.91 0 0); --sidebar-ring: oklch(0.52 0.2 264); } @@ -82,7 +82,7 @@ } :root { - --radius: 1rem; + --radius: 0.75rem; } :root:not(.dark) { diff --git a/apps/web/src/theme.css b/apps/web/src/theme.css index 96632c2ca..913cff4ee 100644 --- a/apps/web/src/theme.css +++ b/apps/web/src/theme.css @@ -38,6 +38,16 @@ --color-card: var(--card); --color-foreground: var(--foreground); --color-background: var(--background); + --shadow-2xs: 0 1px 0 0 rgb(0 0 0 / 0.03); + --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.04); + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.04), 0 1px 3px 0 rgb(0 0 0 / 0.05); + --shadow-md: + 0 2px 4px -1px rgb(0 0 0 / 0.04), 0 6px 16px -4px rgb(0 0 0 / 0.08); + --shadow-lg: + 0 4px 8px -2px rgb(0 0 0 / 0.04), 0 12px 28px -6px rgb(0 0 0 / 0.1); + --shadow-xl: + 0 8px 16px -4px rgb(0 0 0 / 0.05), 0 24px 48px -12px rgb(0 0 0 / 0.14); + --inset-shadow-highlight: inset 0 1px 0 0 rgb(255 255 255 / 0.16); --radius-sm: calc(var(--radius) * 0.6); --radius-md: calc(var(--radius) * 0.8); --radius-lg: var(--radius); diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/styles.css b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/styles.css index 526224bb1..f7e12b6a6 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/styles.css +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/styles.css @@ -7,38 +7,38 @@ :root:not(.dark) { color-scheme: light; - --background: oklch(0.96 0.007 264); - --foreground: oklch(0.23 0.021 264); + --background: oklch(0.97 0 0); + --foreground: oklch(0.205 0 0); --card: oklch(1 0 0); - --card-foreground: oklch(0.23 0.021 264); + --card-foreground: oklch(0.205 0 0); --popover: oklch(1 0 0); - --popover-foreground: oklch(0.23 0.021 264); + --popover-foreground: oklch(0.205 0 0); --primary: oklch(0.52 0.2 264); --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.935 0.005 264); - --secondary-foreground: oklch(0.394 0.02 264); - --muted: oklch(0.935 0.005 264); - --muted-foreground: oklch(0.46 0.017 264); - --accent: oklch(0.92 0.008 264); - --accent-foreground: oklch(0.23 0.021 264); + --secondary: oklch(0.95 0 0); + --secondary-foreground: oklch(0.269 0 0); + --muted: oklch(0.95 0 0); + --muted-foreground: oklch(0.52 0 0); + --accent: oklch(0.95 0 0); + --accent-foreground: oklch(0.205 0 0); --destructive: oklch(0.565 0.209 27); --success: oklch(0.52 0.128 155); --warn: oklch(0.55 0.145 50); - --border: oklch(0.915 0.004 264); - --input: oklch(0.878 0.005 264); + --border: oklch(0.925 0 0); + --input: oklch(0.9 0 0); --ring: oklch(0.52 0.2 264); --chart-1: oklch(0.52 0.2 264); --chart-2: oklch(0.52 0.128 155); --chart-3: oklch(0.565 0.209 27); --chart-4: oklch(0.55 0.145 50); - --chart-5: oklch(0.565 0.017 264); - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.394 0.02 264); + --chart-5: oklch(0.6 0 0); + --sidebar: oklch(0.955 0 0); + --sidebar-foreground: oklch(0.46 0 0); --sidebar-primary: oklch(0.52 0.2 264); --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.92 0.008 264); - --sidebar-accent-foreground: oklch(0.23 0.021 264); - --sidebar-border: oklch(0.915 0.004 264); + --sidebar-accent: oklch(1 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.91 0 0); --sidebar-ring: oklch(0.52 0.2 264); } @@ -80,7 +80,7 @@ } :root { - --radius: 1rem; + --radius: 0.75rem; } @theme inline { @@ -122,6 +122,16 @@ --color-card: var(--card); --color-foreground: var(--foreground); --color-background: var(--background); + --shadow-2xs: 0 1px 0 0 rgb(0 0 0 / 0.03); + --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.04); + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.04), 0 1px 3px 0 rgb(0 0 0 / 0.05); + --shadow-md: + 0 2px 4px -1px rgb(0 0 0 / 0.04), 0 6px 16px -4px rgb(0 0 0 / 0.08); + --shadow-lg: + 0 4px 8px -2px rgb(0 0 0 / 0.04), 0 12px 28px -6px rgb(0 0 0 / 0.1); + --shadow-xl: + 0 8px 16px -4px rgb(0 0 0 / 0.05), 0 24px 48px -12px rgb(0 0 0 / 0.14); + --inset-shadow-highlight: inset 0 1px 0 0 rgb(255 255 255 / 0.16); --radius-sm: calc(var(--radius) * 0.6); --radius-md: calc(var(--radius) * 0.8); --radius-lg: var(--radius); diff --git a/packages/vitnode/src/api/adapters/sso/discord.ts b/packages/vitnode/src/api/adapters/sso/discord.ts index 8461e3646..617d3f16c 100644 --- a/packages/vitnode/src/api/adapters/sso/discord.ts +++ b/packages/vitnode/src/api/adapters/sso/discord.ts @@ -9,6 +9,22 @@ import { getRedirectUri } from "@/api/models/sso"; import { DISCORD_ICON } from "./icons"; +const DISCORD_AVATAR_HASH = /^(?:a_)?[\da-f]{16,64}$/i; + +const discordAvatarUrl = ({ + avatar, + id, +}: { + avatar?: null | string; + id: string; +}): null | string => { + if (!(avatar && DISCORD_AVATAR_HASH.test(avatar) && /^\d+$/.test(id))) { + return null; + } + + return `https://cdn.discordapp.com/avatars/${id}/${avatar}.png?size=512`; +}; + export const DiscordSSOApiPlugin = ({ clientId = "", clientSecret = "", @@ -23,6 +39,7 @@ export const DiscordSSOApiPlugin = ({ email: z.string(), username: z.string(), verified: z.boolean(), + avatar: z.string().nullable().optional(), }); const tokenSchema = z.object({ access_token: z.string(), @@ -91,7 +108,12 @@ export const DiscordSSOApiPlugin = ({ }); } - return data; + return { + avatarUrl: discordAvatarUrl(data), + email: data.email, + id: data.id, + username: data.username, + }; }, getUrl: ({ state }) => { if (!clientId) { @@ -110,5 +132,6 @@ export const DiscordSSOApiPlugin = ({ icon: DISCORD_ICON, id, name: "Discord", + profileFields: ["avatar"], }; }; diff --git a/packages/vitnode/src/api/adapters/sso/facebook.ts b/packages/vitnode/src/api/adapters/sso/facebook.ts index 4305de6c8..fbdc3677d 100644 --- a/packages/vitnode/src/api/adapters/sso/facebook.ts +++ b/packages/vitnode/src/api/adapters/sso/facebook.ts @@ -9,6 +9,11 @@ import { getRedirectUri } from "@/api/models/sso"; import { FACEBOOK_ICON } from "./icons"; +export const FACEBOOK_GRAPH_VERSION = "v26.0"; + +const FACEBOOK_USER_FIELDS = + "id,name,email,first_name,last_name,picture.width(512).height(512){url,is_silhouette}"; + export const FacebookSSOApiPlugin = ({ clientId, clientSecret, @@ -26,19 +31,30 @@ export const FacebookSSOApiPlugin = ({ id: z.string(), name: z.string(), email: z.string(), + first_name: z.string().optional(), + last_name: z.string().optional(), + picture: z + .object({ + data: z.object({ + is_silhouette: z.boolean().optional(), + url: z.string().optional(), + }), + }) + .optional(), }); return { icon: FACEBOOK_ICON, id, name: "Facebook", + profileFields: ["avatar", "firstName", "lastName"], fetchToken: async code => { if (!(clientId && clientSecret)) { throw new Error("Missing Facebook client ID or secret"); } const url = new URL( - "https://graph.facebook.com/v22.0/oauth/access_token", + `https://graph.facebook.com/${FACEBOOK_GRAPH_VERSION}/oauth/access_token`, ); url.searchParams.set("code", code); url.searchParams.set("redirect_uri", redirectUri); @@ -65,8 +81,10 @@ export const FacebookSSOApiPlugin = ({ }, fetchUser: async ({ access_token }) => { - const url = new URL("https://graph.facebook.com/v22.0/me"); - url.searchParams.set("fields", "id,name,email"); + const url = new URL( + `https://graph.facebook.com/${FACEBOOK_GRAPH_VERSION}/me`, + ); + url.searchParams.set("fields", FACEBOOK_USER_FIELDS); url.searchParams.set("access_token", access_token); const res = await fetch(url.toString()); if (!res.ok) { @@ -86,7 +104,17 @@ export const FacebookSSOApiPlugin = ({ }); } - return { ...userData, username: userData.name }; + const picture = userData.picture?.data; + + return { + avatarUrl: + picture?.is_silhouette === false ? (picture.url ?? null) : null, + email: userData.email, + firstName: userData.first_name ?? null, + id: userData.id, + lastName: userData.last_name ?? null, + username: userData.name, + }; }, getUrl: ({ state }) => { @@ -94,7 +122,9 @@ export const FacebookSSOApiPlugin = ({ throw new Error("Missing Facebook client ID"); } - const url = new URL("https://www.facebook.com/v22.0/dialog/oauth"); + const url = new URL( + `https://www.facebook.com/${FACEBOOK_GRAPH_VERSION}/dialog/oauth`, + ); url.searchParams.set("client_id", clientId); url.searchParams.set("redirect_uri", redirectUri); url.searchParams.set("scope", "public_profile,email"); diff --git a/packages/vitnode/src/api/adapters/sso/google.ts b/packages/vitnode/src/api/adapters/sso/google.ts index e82c5668d..9fe004a71 100644 --- a/packages/vitnode/src/api/adapters/sso/google.ts +++ b/packages/vitnode/src/api/adapters/sso/google.ts @@ -27,12 +27,16 @@ export const GoogleSSOApiPlugin = ({ email: z.string(), name: z.string(), verified_email: z.boolean(), + given_name: z.string().optional(), + family_name: z.string().optional(), + picture: z.string().optional(), }); return { icon: GOOGLE_ICON, id, name: "Google", + profileFields: ["avatar", "firstName", "lastName"], fetchToken: async code => { if (!(clientId && clientSecret)) { throw new Error("Missing Google client ID or secret"); @@ -92,7 +96,11 @@ export const GoogleSSOApiPlugin = ({ } return { - ...data, + avatarUrl: data.picture ?? null, + email: data.email, + firstName: data.given_name ?? null, + id: data.id, + lastName: data.family_name ?? null, username: data.name, }; }, diff --git a/packages/vitnode/src/api/adapters/sso/profile.test.ts b/packages/vitnode/src/api/adapters/sso/profile.test.ts new file mode 100644 index 000000000..690c8892e --- /dev/null +++ b/packages/vitnode/src/api/adapters/sso/profile.test.ts @@ -0,0 +1,218 @@ +// @vitest-environment node +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { normalizeSsoProfile } from "@/api/lib/sso-profile"; + +import { DiscordSSOApiPlugin } from "./discord"; +import { FACEBOOK_GRAPH_VERSION, FacebookSSOApiPlugin } from "./facebook"; +import { GoogleSSOApiPlugin } from "./google"; + +const credentials = { clientId: "client", clientSecret: "secret" }; + +const TOKEN = { access_token: "token", token_type: "Bearer" }; + +const answer = (body: unknown) => { + const fetch = vi.fn(async (_input: unknown) => + Promise.resolve(new Response(JSON.stringify(body), { status: 200 })), + ); + vi.stubGlobal("fetch", fetch); + + return fetch; +}; + +const scopeOf = (url: string) => new URL(url).searchParams.get("scope"); + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe("the Google adapter", () => { + const google = GoogleSSOApiPlugin(credentials); + + it("returns the picture, given name and family name", async () => { + answer({ + email: "alice@gmail.test", + family_name: "Smith", + given_name: "Alice", + id: "123", + name: "Alice Smith", + picture: "https://lh3.googleusercontent.com/a/photo=s96-c", + verified_email: true, + }); + + expect(await google.fetchUser(TOKEN)).toEqual({ + avatarUrl: "https://lh3.googleusercontent.com/a/photo=s96-c", + email: "alice@gmail.test", + firstName: "Alice", + id: "123", + lastName: "Smith", + username: "Alice Smith", + }); + }); + + it("treats a missing given or family name as absent, never splitting the full name", async () => { + answer({ + email: "alice@gmail.test", + id: "123", + name: "Alice Smith", + verified_email: true, + }); + + expect(await google.fetchUser(TOKEN)).toMatchObject({ + avatarUrl: null, + firstName: null, + lastName: null, + }); + }); + + it("asks for no more than it did before", () => { + expect(scopeOf(google.getUrl({ state: "s" }))).toBe("openid profile email"); + expect(google.profileFields).toEqual(["avatar", "firstName", "lastName"]); + }); +}); + +describe("the Discord adapter", () => { + const discord = DiscordSSOApiPlugin(credentials); + + it("builds the CDN avatar URL from the avatar hash", async () => { + answer({ + avatar: "a_1269e74af4df7417b13759eae50c83dc", + email: "alice@discord.test", + global_name: "Alice Smith", + id: "80351110224678912", + username: "alice", + verified: true, + }); + + expect(await discord.fetchUser(TOKEN)).toEqual({ + avatarUrl: + "https://cdn.discordapp.com/avatars/80351110224678912/a_1269e74af4df7417b13759eae50c83dc.png?size=512", + email: "alice@discord.test", + id: "80351110224678912", + username: "alice", + }); + }); + + it("reports no avatar for a default one, or a hash it cannot trust", async () => { + for (const avatar of [null, "../../evil", undefined]) { + answer({ + avatar, + email: "alice@discord.test", + id: "80351110224678912", + username: "alice", + verified: true, + }); + + expect((await discord.fetchUser(TOKEN)).avatarUrl).toBeNull(); + } + }); + + it("never offers a first or last name", () => { + expect(discord.profileFields).toEqual(["avatar"]); + expect( + normalizeSsoProfile(discord, { + avatarUrl: null, + firstName: "Alice", + lastName: "Smith", + }), + ).toEqual({ avatarUrl: null, firstName: null, lastName: null }); + expect(scopeOf(discord.getUrl({ state: "s" }))).toBe("identify email"); + }); +}); + +describe("the Facebook adapter", () => { + const facebook = FacebookSSOApiPlugin(credentials); + + it("requests the separate name fields and the picture from the current Graph API", async () => { + const fetch = answer({ + email: "alice@facebook.test", + first_name: "Alice", + id: "10", + last_name: "Smith", + name: "Alice Smith", + picture: { + data: { + is_silhouette: false, + url: "https://platform-lookaside.fbsbx.com/photo.jpg", + }, + }, + }); + + expect(await facebook.fetchUser(TOKEN)).toEqual({ + avatarUrl: "https://platform-lookaside.fbsbx.com/photo.jpg", + email: "alice@facebook.test", + firstName: "Alice", + id: "10", + lastName: "Smith", + username: "Alice Smith", + }); + + const requested = new URL(String(fetch.mock.calls[0]?.[0])); + expect(requested.pathname).toBe(`/${FACEBOOK_GRAPH_VERSION}/me`); + expect(requested.searchParams.get("fields")).toContain("first_name"); + expect(requested.searchParams.get("fields")).toContain("last_name"); + expect(requested.searchParams.get("fields")).toContain("is_silhouette"); + }); + + it("treats the default silhouette as no avatar", async () => { + answer({ + email: "alice@facebook.test", + id: "10", + name: "Alice Smith", + picture: { data: { is_silhouette: true, url: "https://x.test/s.jpg" } }, + }); + + expect(await facebook.fetchUser(TOKEN)).toMatchObject({ + avatarUrl: null, + firstName: null, + lastName: null, + }); + }); + + it("asks for public_profile and email only", () => { + expect(scopeOf(facebook.getUrl({ state: "s" }))).toBe( + "public_profile,email", + ); + }); +}); + +describe("normalizing what an adapter returns", () => { + const everything = { + profileFields: ["avatar", "firstName", "lastName"] as const, + }; + + it("keeps only https avatar URLs", () => { + for (const avatarUrl of [ + "http://example.test/a.png", + "javascript:alert(1)", + "data:image/png;base64,AAAA", + "not a url", + ]) { + expect( + normalizeSsoProfile(everything, { avatarUrl }).avatarUrl, + ).toBeNull(); + } + }); + + it("drops empty and over-long names instead of storing them", () => { + expect( + normalizeSsoProfile(everything, { + firstName: " ", + lastName: "x".repeat(500), + }), + ).toEqual({ avatarUrl: null, firstName: null, lastName: null }); + }); + + it("ignores everything from an adapter that declares no profile fields", () => { + expect( + normalizeSsoProfile( + {}, + { + avatarUrl: "https://example.test/a.png", + firstName: "Alice", + lastName: "Smith", + }, + ), + ).toEqual({ avatarUrl: null, firstName: null, lastName: null }); + }); +}); diff --git a/packages/vitnode/src/api/lib/db-errors.ts b/packages/vitnode/src/api/lib/db-errors.ts new file mode 100644 index 000000000..0cd06da49 --- /dev/null +++ b/packages/vitnode/src/api/lib/db-errors.ts @@ -0,0 +1,23 @@ +const postgresCodeOf = (error: unknown): string | undefined => { + let current: unknown = error; + + for (let depth = 0; depth < 4 && current; depth++) { + if (typeof current === "object" && "code" in current) { + const { code } = current; + if (typeof code === "string") return code; + } + + current = + typeof current === "object" && "cause" in current + ? current.cause + : undefined; + } + + return undefined; +}; + +export const isUniqueViolation = (error: unknown): boolean => + postgresCodeOf(error) === "23505"; + +export const isForeignKeyViolation = (error: unknown): boolean => + postgresCodeOf(error) === "23503"; diff --git a/packages/vitnode/src/api/lib/remote-image.test.ts b/packages/vitnode/src/api/lib/remote-image.test.ts new file mode 100644 index 000000000..f62e8dc34 --- /dev/null +++ b/packages/vitnode/src/api/lib/remote-image.test.ts @@ -0,0 +1,320 @@ +// @vitest-environment node +import { describe, expect, it, vi } from "vitest"; + +import type { RemoteImageResponse, RemoteImageTransport } from "./remote-image"; + +import { + downloadRemoteImage, + httpsImageTransport, + isPublicIpAddress, + publicOnlyLookup, + REMOTE_IMAGE_MAX_BYTES, + RemoteImageError, + sniffImageMimeType, +} from "./remote-image"; + +const png = (width: number, height: number, extra = 0): Uint8Array => { + const bytes = new Uint8Array(33 + extra); + bytes.set([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], 0); + bytes.set([0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52], 8); + const view = new DataView(bytes.buffer); + view.setUint32(16, width); + view.setUint32(20, height); + + return bytes; +}; + +const reply = ( + status: number, + { + body = new Uint8Array(), + contentLength = null, + location = null, + }: { + body?: Uint8Array; + contentLength?: null | number; + location?: null | string; + } = {}, +): RemoteImageResponse => ({ + body: (async function* chunks() { + await Promise.resolve(); + yield body; + })(), + contentLength, + destroy: () => undefined, + location, + status, +}); + +const PUBLIC_HOSTS: Record = { + "cdn.discordapp.com": ["162.159.130.233"], + "lh3.googleusercontent.com": ["142.250.186.65"], + "rebind.example.com": ["10.0.0.5"], + "mixed.example.com": ["142.250.186.65", "127.0.0.1"], +}; + +const resolveHost = async (hostname: string) => + Promise.resolve(PUBLIC_HOSTS[hostname] ?? []); + +const download = async ( + url: string, + transport: RemoteImageTransport, + maxBytes = 1024 * 1024, +) => await downloadRemoteImage(url, { maxBytes, resolveHost, transport }); + +const failure = async (promise: Promise) => { + try { + await promise; + } catch (error) { + if (error instanceof RemoteImageError) return error.reason; + throw error; + } + + return "resolved"; +}; + +describe("telling public addresses from private ones", () => { + it.each([ + "127.0.0.1", + "10.1.2.3", + "172.16.0.1", + "192.168.1.1", + "169.254.169.254", + "100.64.0.1", + "0.0.0.0", + "224.0.0.1", + "::1", + "::", + "fe80::1", + "fd00::1", + "::ffff:127.0.0.1", + "::ffff:7f00:1", + "64:ff9b::a00:1", + "not-an-ip", + ])("refuses %s", address => { + expect(isPublicIpAddress(address)).toBe(false); + }); + + it.each(["142.250.186.65", "162.159.130.233", "2606:4700::6810:84e5"])( + "accepts %s", + address => { + expect(isPublicIpAddress(address)).toBe(true); + }, + ); +}); + +describe("downloading a provider avatar", () => { + it("returns the bytes of a real image with its sniffed type", async () => { + const image = await download( + "https://lh3.googleusercontent.com/a/photo", + async () => Promise.resolve(reply(200, { body: png(96, 96) })), + ); + + expect(image).toMatchObject({ + height: 96, + mimeType: "image/png", + width: 96, + }); + }); + + it("only speaks https", async () => { + const transport = vi.fn(); + + expect( + await failure(download("http://lh3.googleusercontent.com/a", transport)), + ).toBe("invalid_url"); + expect(await failure(download("file:///etc/passwd", transport))).toBe( + "invalid_url", + ); + expect( + await failure( + download("https://user:pass@lh3.googleusercontent.com/a", transport), + ), + ).toBe("invalid_url"); + expect(transport).not.toHaveBeenCalled(); + }); + + it.each([ + "https://127.0.0.1/avatar.png", + "https://[::1]/avatar.png", + "https://169.254.169.254/latest/meta-data", + "https://localhost/avatar.png", + "https://rebind.example.com/avatar.png", + "https://mixed.example.com/avatar.png", + "https://unknown.example.com/avatar.png", + ])("never connects to %s", async url => { + const transport = vi.fn(); + + expect(await failure(download(url, transport))).toBe("blocked_address"); + expect(transport).not.toHaveBeenCalled(); + }); + + it("re-checks every redirect, and refuses one into the private network", async () => { + const transport = vi.fn(async () => + Promise.resolve( + reply(302, { location: "https://169.254.169.254/latest/meta-data" }), + ), + ); + + expect( + await failure( + download("https://cdn.discordapp.com/avatars/1/a.png", transport), + ), + ).toBe("blocked_address"); + expect(transport).toHaveBeenCalledOnce(); + }); + + it("follows a public redirect", async () => { + const transport = vi + .fn() + .mockResolvedValueOnce(reply(301, { location: "/a/photo-large" })) + .mockResolvedValueOnce(reply(200, { body: png(512, 512) })); + + const image = await download( + "https://lh3.googleusercontent.com/a/photo", + transport, + ); + + expect(image.width).toBe(512); + expect(transport.mock.calls[1]?.[0].href).toBe( + "https://lh3.googleusercontent.com/a/photo-large", + ); + }); + + it("gives up after a few redirects", async () => { + const transport = vi.fn(async () => + Promise.resolve(reply(302, { location: "/again" })), + ); + + expect( + await failure(download("https://lh3.googleusercontent.com/a", transport)), + ).toBe("too_many_redirects"); + }); + + it("refuses a body over the limit, whether announced or streamed", async () => { + expect( + await failure( + download( + "https://lh3.googleusercontent.com/a", + async () => + Promise.resolve( + reply(200, { body: png(1, 1), contentLength: 5000 }), + ), + 1000, + ), + ), + ).toBe("too_large"); + expect( + await failure( + download( + "https://lh3.googleusercontent.com/a", + async () => Promise.resolve(reply(200, { body: png(1, 1, 2000) })), + 1000, + ), + ), + ).toBe("too_large"); + }); + + it("never allows more than the hard cap, whatever the role allows", async () => { + const transport = async () => + Promise.resolve( + reply(200, { + body: png(1, 1), + contentLength: REMOTE_IMAGE_MAX_BYTES + 1, + }), + ); + + expect( + await failure( + download( + "https://lh3.googleusercontent.com/a", + transport, + REMOTE_IMAGE_MAX_BYTES * 10, + ), + ), + ).toBe("too_large"); + }); + + it("refuses content that is not a JPEG, PNG or WebP, whatever it claims to be", async () => { + const html = new TextEncoder().encode("not an image"); + + expect( + await failure( + download("https://lh3.googleusercontent.com/a", async () => + Promise.resolve(reply(200, { body: html })), + ), + ), + ).toBe("unsupported_type"); + }); + + it("refuses an image too large to process", async () => { + expect( + await failure( + download("https://lh3.googleusercontent.com/a", async () => + Promise.resolve(reply(200, { body: png(20_000, 20_000) })), + ), + ), + ).toBe("unsupported_type"); + }); + + it("reports a failed response as an error rather than an image", async () => { + expect( + await failure( + download("https://lh3.googleusercontent.com/a", async () => + Promise.resolve(reply(404)), + ), + ), + ).toBe("http_error"); + expect( + await failure( + download("https://lh3.googleusercontent.com/a", async () => + Promise.reject(new Error("socket hang up")), + ), + ), + ).toBe("network"); + }); +}); + +describe("sniffing an image type", () => { + it("reads the magic bytes", () => { + expect(sniffImageMimeType(new Uint8Array([0xff, 0xd8, 0xff, 0xe0]))).toBe( + "image/jpeg", + ); + expect(sniffImageMimeType(png(1, 1))).toBe("image/png"); + expect( + sniffImageMimeType( + new TextEncoder().encode("RIFF\u0000\u0000\u0000\u0000WEBPVP8 "), + ), + ).toBe("image/webp"); + expect(sniffImageMimeType(new TextEncoder().encode("GIF89a"))).toBeNull(); + }); +}); + +describe("the connection-time address check", () => { + it("refuses to connect when the name resolves to a private address at connect time", async () => { + const error = await new Promise(resolve => { + publicOnlyLookup("localhost", {}, lookupError => { + resolve(lookupError); + }); + }); + + expect(error).toBeInstanceOf(RemoteImageError); + expect((error as RemoteImageError).reason).toBe("blocked_address"); + }); + + it("stops the real transport even when the pre-check was fooled", async () => { + const reason = await failure( + downloadRemoteImage("https://localhost.example.test/avatar.png", { + maxBytes: 1024, + resolveHost: async () => Promise.resolve(["142.250.186.65"]), + transport: async (url, options) => + await httpsImageTransport(new URL("https://localhost/avatar.png"), { + ...options, + signal: AbortSignal.timeout(2000), + }), + }), + ); + + expect(reason).toBe("blocked_address"); + }); +}); diff --git a/packages/vitnode/src/api/lib/remote-image.ts b/packages/vitnode/src/api/lib/remote-image.ts new file mode 100644 index 000000000..8b0874055 --- /dev/null +++ b/packages/vitnode/src/api/lib/remote-image.ts @@ -0,0 +1,472 @@ +import type { LookupAddress } from "node:dns"; +import type { LookupFunction } from "node:net"; + +import { lookup as dnsLookup } from "node:dns"; +import { request as httpsRequest } from "node:https"; +import { BlockList, isIP } from "node:net"; + +export const REMOTE_IMAGE_MAX_BYTES = 5 * 1024 * 1024; + +const REMOTE_IMAGE_MAX_DIMENSION = 8192; + +const REMOTE_IMAGE_MAX_PIXELS = 40_000_000; + +const DEFAULT_MAX_REDIRECTS = 3; + +const DEFAULT_TIMEOUT_MS = 8000; + +const MAX_URL_LENGTH = 2048; + +export type RemoteImageFailure = + | "blocked_address" + | "http_error" + | "invalid_url" + | "network" + | "too_large" + | "too_many_redirects" + | "unsupported_type"; + +export class RemoteImageError extends Error { + constructor(reason: RemoteImageFailure) { + super(`Remote image rejected: ${reason}`); + this.name = "RemoteImageError"; + this.reason = reason; + } + + readonly reason: RemoteImageFailure; +} + +const blockedAddresses = (() => { + const list = new BlockList(); + const v4: [string, number][] = [ + ["0.0.0.0", 8], + ["10.0.0.0", 8], + ["100.64.0.0", 10], + ["127.0.0.0", 8], + ["169.254.0.0", 16], + ["172.16.0.0", 12], + ["192.0.0.0", 24], + ["192.0.2.0", 24], + ["192.88.99.0", 24], + ["192.168.0.0", 16], + ["198.18.0.0", 15], + ["198.51.100.0", 24], + ["203.0.113.0", 24], + ["224.0.0.0", 4], + ["240.0.0.0", 4], + ]; + const v6: [string, number][] = [ + ["::", 128], + ["::1", 128], + ["64:ff9b::", 96], + ["64:ff9b:1::", 48], + ["100::", 64], + ["2001::", 23], + ["2001:db8::", 32], + ["2002::", 16], + ["fc00::", 7], + ["fe80::", 10], + ["fec0::", 10], + ["ff00::", 8], + ]; + + for (const [address, prefix] of v4) list.addSubnet(address, prefix, "ipv4"); + for (const [address, prefix] of v6) list.addSubnet(address, prefix, "ipv6"); + + return list; +})(); + +const MAPPED_IPV4 = /^::ffff:(?\d{1,3}(?:\.\d{1,3}){3})$/i; + +const mappedHexIpv4 = (address: string): null | string => { + const match = /^::ffff:(?[\da-f]{1,4}):(?[\da-f]{1,4})$/i.exec( + address, + ); + if (!match?.groups) return null; + + const high = Number.parseInt(match.groups.high ?? "0", 16); + const low = Number.parseInt(match.groups.low ?? "0", 16); + + return [high >> 8, high & 255, low >> 8, low & 255].join("."); +}; + +export const isPublicIpAddress = (raw: string): boolean => { + const address = raw.replace(/^\[|\]$/g, "").split("%")[0] ?? ""; + const version = isIP(address); + if (version === 0) return false; + + if (version === 6) { + const embedded = + MAPPED_IPV4.exec(address)?.groups?.v4 ?? mappedHexIpv4(address); + if (embedded) return isPublicIpAddress(embedded); + if (/^::ffff:/i.test(address)) return false; + + return !blockedAddresses.check(address, "ipv6"); + } + + if (address === "255.255.255.255") return false; + + return !blockedAddresses.check(address, "ipv4"); +}; + +export type ResolveHost = (hostname: string) => Promise; + +const resolveHostAddresses: ResolveHost = async hostname => + await new Promise((resolve, reject) => { + dnsLookup(hostname, { all: true }, (error, addresses) => { + if (error) { + reject(new RemoteImageError("network")); + + return; + } + + resolve(addresses.map(entry => entry.address)); + }); + }); + +export const publicOnlyLookup: LookupFunction = ( + hostname, + options, + callback, +) => { + dnsLookup( + hostname, + { ...options, all: true }, + (error, addresses: LookupAddress[]) => { + if (error) { + callback(error, "", 4); + + return; + } + + const [first] = addresses; + if ( + !first || + addresses.some(entry => !isPublicIpAddress(entry.address)) + ) { + callback(new RemoteImageError("blocked_address"), "", 4); + + return; + } + + if (options.all) { + callback(null, addresses); + + return; + } + + callback(null, first.address, first.family); + }, + ); +}; + +export interface RemoteImageResponse { + body: AsyncIterable; + contentLength: null | number; + destroy: () => void; + location: null | string; + status: number; +} + +export type RemoteImageTransport = ( + url: URL, + options: { lookup: LookupFunction; signal: AbortSignal }, +) => Promise; + +export const httpsImageTransport: RemoteImageTransport = async ( + url, + { lookup, signal }, +) => + await new Promise((resolve, reject) => { + const request = httpsRequest( + url, + { + agent: false, + headers: { + accept: "image/webp,image/png,image/jpeg", + "user-agent": "VitNode avatar import", + }, + lookup, + method: "GET", + signal, + }, + response => { + const length = Number(response.headers["content-length"]); + + resolve({ + body: response, + contentLength: Number.isFinite(length) ? length : null, + destroy: () => response.destroy(), + location: response.headers.location ?? null, + status: response.statusCode ?? 0, + }); + }, + ); + + request.on("error", error => { + reject( + error instanceof RemoteImageError + ? error + : new RemoteImageError("network"), + ); + }); + request.end(); + }); + +export type RemoteImageMimeType = "image/jpeg" | "image/png" | "image/webp"; + +export const sniffImageMimeType = ( + bytes: Uint8Array, +): null | RemoteImageMimeType => { + const starts = (signature: number[], offset = 0) => + signature.every((byte, index) => bytes[offset + index] === byte); + + if (starts([0xff, 0xd8, 0xff])) return "image/jpeg"; + if (starts([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) { + return "image/png"; + } + if (starts([0x52, 0x49, 0x46, 0x46]) && starts([0x57, 0x45, 0x42, 0x50], 8)) { + return "image/webp"; + } + + return null; +}; + +const readUint16BE = (bytes: Uint8Array, offset: number) => + ((bytes[offset] ?? 0) << 8) | (bytes[offset + 1] ?? 0); + +const readUint24LE = (bytes: Uint8Array, offset: number) => + (bytes[offset] ?? 0) | + ((bytes[offset + 1] ?? 0) << 8) | + ((bytes[offset + 2] ?? 0) << 16); + +const readUint32BE = (bytes: Uint8Array, offset: number) => + readUint16BE(bytes, offset) * 65536 + readUint16BE(bytes, offset + 2); + +const JPEG_FRAME_MARKERS = new Set([ + 0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf, +]); + +const jpegDimensions = (bytes: Uint8Array) => { + let offset = 2; + + while (offset + 9 < bytes.length) { + if (bytes[offset] !== 0xff) return null; + const marker = bytes[offset + 1] ?? 0; + if (marker === 0xff) { + offset += 1; + continue; + } + if (marker === 0xd9 || marker === 0xda) return null; + + const length = readUint16BE(bytes, offset + 2); + if (JPEG_FRAME_MARKERS.has(marker)) { + return { + height: readUint16BE(bytes, offset + 5), + width: readUint16BE(bytes, offset + 7), + }; + } + offset += 2 + length; + } + + return null; +}; + +const webpDimensions = (bytes: Uint8Array) => { + const chunk = String.fromCharCode(...bytes.slice(12, 16)); + + if (chunk === "VP8X") { + return { + height: readUint24LE(bytes, 27) + 1, + width: readUint24LE(bytes, 24) + 1, + }; + } + if (chunk === "VP8L") { + const b0 = bytes[21] ?? 0; + const b1 = bytes[22] ?? 0; + const b2 = bytes[23] ?? 0; + const b3 = bytes[24] ?? 0; + + return { + height: 1 + (((b3 & 0xf) << 10) | (b2 << 2) | ((b1 & 0xc0) >> 6)), + width: 1 + (((b1 & 0x3f) << 8) | b0), + }; + } + if (chunk === "VP8 ") { + return { + height: (((bytes[29] ?? 0) << 8) | (bytes[28] ?? 0)) & 0x3fff, + width: (((bytes[27] ?? 0) << 8) | (bytes[26] ?? 0)) & 0x3fff, + }; + } + + return null; +}; + +export const imageDimensions = ( + bytes: Uint8Array, + mimeType: RemoteImageMimeType, +): null | { height: number; width: number } => { + if (mimeType === "image/png") { + if (bytes.length < 24) return null; + + return { height: readUint32BE(bytes, 20), width: readUint32BE(bytes, 16) }; + } + if (mimeType === "image/webp") return webpDimensions(bytes); + + return jpegDimensions(bytes); +}; + +export const IMAGE_EXTENSIONS: Record = { + "image/jpeg": "jpg", + "image/png": "png", + "image/webp": "webp", +}; + +export interface RemoteImage { + bytes: Uint8Array; + height: number; + mimeType: RemoteImageMimeType; + width: number; +} + +const assertFetchableUrl = async ( + url: URL, + resolveHost: ResolveHost, +): Promise => { + if (url.protocol !== "https:") throw new RemoteImageError("invalid_url"); + if (url.username || url.password) throw new RemoteImageError("invalid_url"); + if (url.port && url.port !== "443") throw new RemoteImageError("invalid_url"); + + const hostname = url.hostname.replace(/^\[|\]$/g, ""); + if (isIP(hostname) !== 0) { + if (!isPublicIpAddress(hostname)) { + throw new RemoteImageError("blocked_address"); + } + + return; + } + if (!hostname.includes(".") || hostname.endsWith(".localhost")) { + throw new RemoteImageError("blocked_address"); + } + + const addresses = await resolveHost(hostname); + if ( + addresses.length === 0 || + addresses.some(address => !isPublicIpAddress(address)) + ) { + throw new RemoteImageError("blocked_address"); + } +}; + +const parseUrl = (raw: string, base?: URL): URL => { + if (raw.length > MAX_URL_LENGTH) throw new RemoteImageError("invalid_url"); + + try { + return new URL(raw, base); + } catch { + throw new RemoteImageError("invalid_url"); + } +}; + +const readCapped = async ( + body: AsyncIterable, + maxBytes: number, +): Promise> => { + const chunks: Uint8Array[] = []; + let size = 0; + + for await (const chunk of body) { + size += chunk.byteLength; + if (size > maxBytes) throw new RemoteImageError("too_large"); + chunks.push(chunk); + } + + const bytes = new Uint8Array(size); + let offset = 0; + for (const chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; + } + + return bytes; +}; + +export const downloadRemoteImage = async ( + rawUrl: string, + { + maxBytes, + maxRedirects = DEFAULT_MAX_REDIRECTS, + resolveHost = resolveHostAddresses, + timeoutMs = DEFAULT_TIMEOUT_MS, + transport = httpsImageTransport, + }: { + maxBytes: number; + maxRedirects?: number; + resolveHost?: ResolveHost; + timeoutMs?: number; + transport?: RemoteImageTransport; + }, +): Promise => { + const limit = Math.min(maxBytes, REMOTE_IMAGE_MAX_BYTES); + const signal = AbortSignal.timeout(timeoutMs); + let url = parseUrl(rawUrl); + + for (let hop = 0; hop <= maxRedirects; hop++) { + await assertFetchableUrl(url, resolveHost); + + let response: RemoteImageResponse; + try { + response = await transport(url, { lookup: publicOnlyLookup, signal }); + } catch (error) { + if (error instanceof RemoteImageError) throw error; + + throw new RemoteImageError("network"); + } + + if (response.status >= 300 && response.status < 400) { + response.destroy(); + if (!response.location) throw new RemoteImageError("http_error"); + url = parseUrl(response.location, url); + continue; + } + + if (response.status !== 200) { + response.destroy(); + throw new RemoteImageError("http_error"); + } + + if (response.contentLength !== null && response.contentLength > limit) { + response.destroy(); + throw new RemoteImageError("too_large"); + } + + let bytes: Uint8Array; + try { + bytes = await readCapped(response.body, limit); + } catch (error) { + response.destroy(); + if (error instanceof RemoteImageError) throw error; + + throw new RemoteImageError("network"); + } + + const mimeType = sniffImageMimeType(bytes); + if (!mimeType) throw new RemoteImageError("unsupported_type"); + + const dimensions = imageDimensions(bytes, mimeType); + if ( + !dimensions || + dimensions.width < 1 || + dimensions.height < 1 || + dimensions.width > REMOTE_IMAGE_MAX_DIMENSION || + dimensions.height > REMOTE_IMAGE_MAX_DIMENSION || + dimensions.width * dimensions.height > REMOTE_IMAGE_MAX_PIXELS + ) { + throw new RemoteImageError("unsupported_type"); + } + + return { bytes, mimeType, ...dimensions }; + } + + throw new RemoteImageError("too_many_redirects"); +}; diff --git a/packages/vitnode/src/api/lib/sso-avatar-import.test.ts b/packages/vitnode/src/api/lib/sso-avatar-import.test.ts new file mode 100644 index 000000000..f06728940 --- /dev/null +++ b/packages/vitnode/src/api/lib/sso-avatar-import.test.ts @@ -0,0 +1,164 @@ +// @vitest-environment node +import type { Context } from "hono"; + +import { describe, expect, it, vi } from "vitest"; + +import type { EnvVitNode } from "@/api/middlewares/global.middleware"; + +import type { RemoteImage } from "./remote-image"; + +import { importProviderAvatar, sha256Hex } from "./sso-avatar-import"; + +const IMAGE: RemoteImage = { + bytes: new Uint8Array([0x89, 0x50, 0x4e, 0x47, 1, 2, 3]), + height: 64, + mimeType: "image/png", + width: 64, +}; + +const avatarContext = () => { + const upload = vi.fn(async (_options: { file: File }) => + Promise.resolve({ id: 42, url: "/uploads/avatars/new.png" }), + ); + const deleteFile = vi.fn(async () => Promise.resolve()); + const emit = vi.fn(async () => Promise.resolve()); + const variables = { + cache: { deleteSystem: vi.fn(async () => Promise.resolve()) }, + db: { + select: () => ({ + from: () => ({ + where: () => ({ + limit: async () => Promise.resolve([{ fileId: 7 }]), + then: (onFulfilled: (rows: unknown[]) => unknown) => + onFulfilled([]), + }), + }), + }), + update: () => ({ + set: () => ({ + where: () => ({ + returning: async () => Promise.resolve([{ id: 1 }]), + }), + }), + }), + }, + events: { emit }, + storage: { deleteFile, upload }, + }; + + return { + c: { + get: (key: keyof typeof variables) => variables[key], + } as unknown as Context, + deleteFile, + emit, + upload, + }; +}; + +const URL_A = "https://lh3.googleusercontent.test/a/photo.png"; + +describe("importing a provider avatar", () => { + it("does not download again while the imported avatar is still the one shown", async () => { + const { c, upload } = avatarContext(); + const download = vi.fn(async () => Promise.resolve(IMAGE)); + + const outcome = await importProviderAvatar( + c, + { + currentAvatarId: 42, + maxBytes: 1024, + previous: { fileId: 42, sha256: "x", sourceUrl: URL_A }, + url: URL_A, + userId: 1, + }, + { download }, + ); + + expect(outcome).toEqual({ status: "unchanged" }); + expect(download).not.toHaveBeenCalled(); + expect(upload).not.toHaveBeenCalled(); + }); + + it("does not store a second copy of the same picture under a new URL", async () => { + const { c, upload } = avatarContext(); + + const outcome = await importProviderAvatar( + c, + { + currentAvatarId: 42, + maxBytes: 1024, + previous: { + fileId: 42, + sha256: sha256Hex(IMAGE.bytes), + sourceUrl: "https://lh3.googleusercontent.test/a/old-url.png", + }, + url: URL_A, + userId: 1, + }, + { download: async () => Promise.resolve(IMAGE) }, + ); + + expect(outcome).toEqual({ status: "unchanged" }); + expect(upload).not.toHaveBeenCalled(); + }); + + it("stores the picture through the avatar pipeline, typed by its content", async () => { + const { c, deleteFile, emit, upload } = avatarContext(); + + const outcome = await importProviderAvatar( + c, + { + currentAvatarId: 7, + maxBytes: 1024, + previous: { + fileId: 42, + sha256: sha256Hex(IMAGE.bytes), + sourceUrl: URL_A, + }, + url: URL_A, + userId: 1, + }, + { download: async () => Promise.resolve(IMAGE) }, + ); + + expect(outcome).toEqual({ + fileId: 42, + sha256: sha256Hex(IMAGE.bytes), + sourceUrl: URL_A, + status: "updated", + }); + const stored = upload.mock.calls[0]?.[0]; + expect(stored).toMatchObject({ + folder: "avatars", + maxBytes: 1024, + userId: 1, + }); + expect(stored?.file.type).toBe("image/png"); + expect(deleteFile).toHaveBeenCalledWith(7, { force: true }); + expect(emit).toHaveBeenCalledWith("user.avatar.updated", { + fileId: 42, + userId: 1, + }); + }); + + it("leaves the current avatar alone when the download fails", async () => { + const { c, deleteFile, upload } = avatarContext(); + + await expect( + importProviderAvatar( + c, + { + currentAvatarId: 7, + maxBytes: 1024, + previous: { fileId: null, sha256: null, sourceUrl: null }, + url: URL_A, + userId: 1, + }, + { download: async () => Promise.reject(new Error("blocked_address")) }, + ), + ).rejects.toThrow("blocked_address"); + expect(upload).not.toHaveBeenCalled(); + expect(deleteFile).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/vitnode/src/api/lib/sso-avatar-import.ts b/packages/vitnode/src/api/lib/sso-avatar-import.ts new file mode 100644 index 000000000..7d137b954 --- /dev/null +++ b/packages/vitnode/src/api/lib/sso-avatar-import.ts @@ -0,0 +1,77 @@ +import type { Context } from "hono"; + +import { createHash } from "node:crypto"; + +import type { EnvVitNode } from "@/api/middlewares/global.middleware"; + +import type { RemoteImage } from "./remote-image"; + +import { downloadRemoteImage, IMAGE_EXTENSIONS } from "./remote-image"; +import { setUserImage } from "./user-images"; + +export interface ImportedAvatarRecord { + fileId: null | number; + sha256: null | string; + sourceUrl: null | string; +} + +export interface AvatarImportRequest { + currentAvatarId: null | number; + maxBytes: number; + previous: ImportedAvatarRecord; + url: string; + userId: number; +} + +export type AvatarImportOutcome = + | { fileId: number; sha256: string; sourceUrl: string; status: "updated" } + | { status: "unchanged" }; + +const stillShowsImport = ({ + currentAvatarId, + previous, +}: Pick): boolean => + previous.fileId !== null && previous.fileId === currentAvatarId; + +export const sha256Hex = (bytes: Uint8Array): string => + createHash("sha256").update(bytes).digest("hex"); + +export const importProviderAvatar = async ( + c: Context, + request: AvatarImportRequest, + { + download = async (url, maxBytes) => + await downloadRemoteImage(url, { maxBytes }), + }: { + download?: (url: string, maxBytes: number) => Promise; + } = {}, +): Promise => { + if (stillShowsImport(request) && request.previous.sourceUrl === request.url) { + return { status: "unchanged" }; + } + + const image = await download(request.url, request.maxBytes); + const sha256 = sha256Hex(image.bytes); + + if (stillShowsImport(request) && request.previous.sha256 === sha256) { + return { status: "unchanged" }; + } + + const stored = await setUserImage(c, { + file: new File( + [image.bytes], + `avatar.${IMAGE_EXTENSIONS[image.mimeType]}`, + { type: image.mimeType }, + ), + kind: "avatar", + maxBytes: request.maxBytes, + userId: request.userId, + }); + + return { + fileId: stored.id, + sha256, + sourceUrl: request.url, + status: "updated", + }; +}; diff --git a/packages/vitnode/src/api/lib/sso-profile.ts b/packages/vitnode/src/api/lib/sso-profile.ts new file mode 100644 index 000000000..af3fe7963 --- /dev/null +++ b/packages/vitnode/src/api/lib/sso-profile.ts @@ -0,0 +1,63 @@ +import { z } from "zod"; + +import type { SSOApiPlugin, SSOProviderUser } from "@/api/models/sso"; +import type { SsoProfileValues } from "@/api/models/sso-connection-store"; +import type { SsoProfileField } from "@/lib/sso-profile"; + +import { SSO_PROFILE_FIELDS } from "@/lib/sso-profile"; +import { + normalizePersonalField, + USER_FIRST_NAME_MAX_LENGTH, + USER_LAST_NAME_MAX_LENGTH, +} from "@/lib/user-personal-information"; + +const AVATAR_URL_MAX_LENGTH = 2048; + +const zodAvatarUrl = z.url({ protocol: /^https$/ }).max(AVATAR_URL_MAX_LENGTH); + +export const supportedProfileFields = ( + adapter: Pick, +): SsoProfileField[] => + SSO_PROFILE_FIELDS.filter(field => adapter.profileFields?.includes(field)); + +const nameOf = (value: unknown, max: number): null | string => { + const normalized = normalizePersonalField(value); + + return normalized && normalized.length <= max ? normalized : null; +}; + +export const normalizeSsoProfile = ( + adapter: Pick, + user: Pick, +): SsoProfileValues => { + const supported = new Set(supportedProfileFields(adapter)); + const avatar = zodAvatarUrl.safeParse(user.avatarUrl); + + return { + avatarUrl: supported.has("avatar") && avatar.success ? avatar.data : null, + firstName: supported.has("firstName") + ? nameOf(user.firstName, USER_FIRST_NAME_MAX_LENGTH) + : null, + lastName: supported.has("lastName") + ? nameOf(user.lastName, USER_LAST_NAME_MAX_LENGTH) + : null, + }; +}; + +export const PROFILE_VALUE_KEY: Record< + SsoProfileField, + keyof SsoProfileValues +> = { + avatar: "avatarUrl", + firstName: "firstName", + lastName: "lastName", +}; + +export const restrictProfile = ( + profile: SsoProfileValues, + fields: readonly SsoProfileField[], +): SsoProfileValues => ({ + avatarUrl: fields.includes("avatar") ? profile.avatarUrl : null, + firstName: fields.includes("firstName") ? profile.firstName : null, + lastName: fields.includes("lastName") ? profile.lastName : null, +}); diff --git a/packages/vitnode/src/api/lib/user-images.ts b/packages/vitnode/src/api/lib/user-images.ts index 438a57571..0d22834c9 100644 --- a/packages/vitnode/src/api/lib/user-images.ts +++ b/packages/vitnode/src/api/lib/user-images.ts @@ -222,7 +222,7 @@ export const setUserImage = async ( maxBytes, userId, }: { file: File; kind: UserImageKind; maxBytes: number; userId: number }, -): Promise<{ url: string }> => { +): Promise<{ id: number; url: string }> => { const previousId = await currentImageId(c, userId, kind); const stored = await c.get("storage").upload({ @@ -254,7 +254,7 @@ export const setUserImage = async ( await discardFile(c, previousId === stored.id ? null : previousId); await afterImageChange(c, { fileId: stored.id, kind, userId }); - return { url: stored.url }; + return { id: stored.id, url: stored.url }; }; export const removeUserImage = async ( diff --git a/packages/vitnode/src/api/models/events.ts b/packages/vitnode/src/api/models/events.ts index 48253eebf..14dc8d868 100644 --- a/packages/vitnode/src/api/models/events.ts +++ b/packages/vitnode/src/api/models/events.ts @@ -3,6 +3,7 @@ import type { Context } from "hono"; import { randomUUID } from "node:crypto"; import type { NavigationLocation } from "@/lib/navigation"; +import type { SsoProfileField } from "@/lib/sso-profile"; import type { EventListenerConfig } from "../lib/events"; @@ -73,6 +74,16 @@ export interface VitNodeEvents { providerId: string; userId: number; }; + "user.sso.profile_synced": { + fields: SsoProfileField[]; + providerId: string; + trigger: "import" | "sign_in" | "sync"; + userId: number; + }; + "user.sso.unlinked": { + providerId: string; + userId: number; + }; "user.updated": { email: string; name: string; diff --git a/packages/vitnode/src/api/models/sso-connection-store.ts b/packages/vitnode/src/api/models/sso-connection-store.ts new file mode 100644 index 000000000..bbcb86393 --- /dev/null +++ b/packages/vitnode/src/api/models/sso-connection-store.ts @@ -0,0 +1,531 @@ +import type { Context } from "hono"; + +import { and, asc, count, eq, gt, inArray, lte, ne } from "drizzle-orm"; + +import type { SsoOperationIntent, SsoProfileField } from "@/lib/sso-profile"; + +import { isForeignKeyViolation, isUniqueViolation } from "@/api/lib/db-errors"; +import { core_users_passkeys } from "@/database/passkeys"; +import { + core_users, + core_users_sso, + core_users_sso_operations, + core_users_sso_profile_sources, +} from "@/database/users"; +import { SSO_PROFILE_FIELDS } from "@/lib/sso-profile"; + +export interface SsoConnectionRecord { + avatarFileId: null | number; + avatarSha256: null | string; + avatarSourceUrl: null | string; + createdAt: Date; + providerAccountId: string; + providerEmail: null | string; + providerId: string; + providerUsername: null | string; + syncOnSignIn: boolean; + userId: number; +} + +export interface SsoProfileSourceRecord { + field: SsoProfileField; + providerId: string; +} + +export interface SsoProfileValues { + avatarUrl: null | string; + firstName: null | string; + lastName: null | string; +} + +export interface SsoOperationRecord { + expiresAt: Date; + fields: SsoProfileField[]; + intent: SsoOperationIntent; + preview: null | SsoProfileValues; + providerAccountId: null | string; + providerId: string; + tokenHash: null | string; + userId: number; +} + +export interface SsoSignInFacts { + hasPassword: boolean; + otherSsoProviderIds: string[]; + passkeys: number; +} + +export interface SsoAccountProfile { + avatarId: null | number; + email: string; + firstName: null | string; + id: number; + lastName: null | string; + name: string; + roleId: number; +} + +export type SsoLinkOutcome = + "already_linked" | "linked" | "provider_in_use" | "taken"; + +export type SsoDisconnectOutcome = "blocked" | "disconnected" | "not_found"; + +export type SsoPreferencesOutcome = "not_connected" | "saved"; + +export interface SsoPreferencesChange { + sources: Partial>; + sync: Record; + userId: number; +} + +export interface SsoConnectionStore { + clearSources: (args: { + fields: SsoProfileField[]; + userId: number; + }) => Promise; + consumeOperation: (tokenHash: string) => Promise; + deleteExpiredOperations: (now: Date) => Promise; + disconnect: (args: { + canDisconnect: (facts: SsoSignInFacts) => boolean; + providerId: string; + userId: number; + }) => Promise; + findConnection: (args: { + providerId: string; + userId: number; + }) => Promise; + findPreview: (args: { + now: Date; + providerId: string; + userId: number; + }) => Promise; + link: (values: { + providerAccountId: string; + providerEmail: null | string; + providerId: string; + providerUsername: null | string; + userId: number; + }) => Promise; + listConnections: (userId: number) => Promise; + listSources: (userId: number) => Promise; + readAccount: (userId: number) => Promise; + recordAvatar: (args: { + fileId: number; + providerId: string; + sha256: string; + sourceUrl: string; + userId: number; + }) => Promise; + recordSignIn: (args: { + providerEmail: null | string; + providerId: string; + providerUsername: null | string; + userId: number; + }) => Promise; + saveOperation: (values: SsoOperationRecord) => Promise; + savePreferences: ( + change: SsoPreferencesChange, + ) => Promise; + savePreview: (values: SsoOperationRecord) => Promise; + signInFacts: ( + userId: number, + ) => Promise>; + takePreview: (args: { + now: Date; + providerId: string; + userId: number; + }) => Promise; + writeNames: (args: { + userId: number; + values: Partial>; + }) => Promise; +} + +type Db = Context["var"]["db"]; + +const toOperation = ( + row: typeof core_users_sso_operations.$inferSelect, +): SsoOperationRecord => ({ + expiresAt: row.expiresAt, + fields: row.fields, + intent: row.intent, + preview: row.preview, + providerAccountId: row.providerAccountId, + providerId: row.providerId, + tokenHash: row.tokenHash, + userId: row.userId, +}); + +const connectionColumns = { + avatarFileId: core_users_sso.avatarFileId, + avatarSha256: core_users_sso.avatarSha256, + avatarSourceUrl: core_users_sso.avatarSourceUrl, + createdAt: core_users_sso.createdAt, + providerAccountId: core_users_sso.providerAccountId, + providerEmail: core_users_sso.providerEmail, + providerId: core_users_sso.providerId, + providerUsername: core_users_sso.providerUsername, + syncOnSignIn: core_users_sso.syncOnSignIn, + userId: core_users_sso.userId, +}; + +const previewOf = (userId: number, providerId: string, now: Date) => + and( + eq(core_users_sso_operations.userId, userId), + eq(core_users_sso_operations.providerId, providerId), + eq(core_users_sso_operations.intent, "preview"), + gt(core_users_sso_operations.expiresAt, now), + ); + +const accountOwner = async ( + db: Pick, + { + providerAccountId, + providerId, + }: { providerAccountId: string; providerId: string }, +): Promise => { + const [owner] = await db + .select({ userId: core_users_sso.userId }) + .from(core_users_sso) + .where( + and( + eq(core_users_sso.providerId, providerId), + eq(core_users_sso.providerAccountId, providerAccountId), + ), + ) + .limit(1); + + return owner?.userId ?? null; +}; + +const ownershipOutcome = ( + ownerId: number, + userId: number, +): Extract => + ownerId === userId ? "already_linked" : "taken"; + +export const drizzleSsoConnectionStore = (db: Db): SsoConnectionStore => ({ + clearSources: async ({ fields, userId }) => { + if (fields.length === 0) return []; + + const rows = await db + .delete(core_users_sso_profile_sources) + .where( + and( + eq(core_users_sso_profile_sources.userId, userId), + inArray(core_users_sso_profile_sources.field, fields), + ), + ) + .returning({ field: core_users_sso_profile_sources.field }); + + return rows.map(row => row.field); + }, + + consumeOperation: async tokenHash => { + const [row] = await db + .delete(core_users_sso_operations) + .where(eq(core_users_sso_operations.tokenHash, tokenHash)) + .returning(); + + return row ? toOperation(row) : null; + }, + + deleteExpiredOperations: async now => { + await db + .delete(core_users_sso_operations) + .where(lte(core_users_sso_operations.expiresAt, now)); + }, + + disconnect: async ({ canDisconnect, providerId, userId }) => + await db.transaction(async tx => { + const [user] = await tx + .select({ password: core_users.password }) + .from(core_users) + .where(eq(core_users.id, userId)) + .for("update"); + + const [connection] = await tx + .select({ providerId: core_users_sso.providerId }) + .from(core_users_sso) + .where( + and( + eq(core_users_sso.userId, userId), + eq(core_users_sso.providerId, providerId), + ), + ); + + if (!(user && connection)) return "not_found"; + + const [[passkeys], others] = await Promise.all([ + tx + .select({ value: count() }) + .from(core_users_passkeys) + .where(eq(core_users_passkeys.userId, userId)), + tx + .select({ providerId: core_users_sso.providerId }) + .from(core_users_sso) + .where( + and( + eq(core_users_sso.userId, userId), + ne(core_users_sso.providerId, providerId), + ), + ), + ]); + + const allowed = canDisconnect({ + hasPassword: !!user.password, + otherSsoProviderIds: others.map(row => row.providerId), + passkeys: passkeys?.value ?? 0, + }); + + if (!allowed) return "blocked"; + + await tx + .delete(core_users_sso_operations) + .where( + and( + eq(core_users_sso_operations.userId, userId), + eq(core_users_sso_operations.providerId, providerId), + ), + ); + await tx + .delete(core_users_sso) + .where( + and( + eq(core_users_sso.userId, userId), + eq(core_users_sso.providerId, providerId), + ), + ); + + return "disconnected"; + }), + + findConnection: async ({ providerId, userId }) => { + const [row] = await db + .select(connectionColumns) + .from(core_users_sso) + .where( + and( + eq(core_users_sso.userId, userId), + eq(core_users_sso.providerId, providerId), + ), + ) + .limit(1); + + return row ?? null; + }, + + findPreview: async ({ now, providerId, userId }) => { + const [row] = await db + .select() + .from(core_users_sso_operations) + .where(previewOf(userId, providerId, now)) + .limit(1); + + return row ? toOperation(row) : null; + }, + + link: async values => { + try { + return await db.transaction(async tx => { + const ownerId = await accountOwner(tx, values); + if (ownerId !== null) return ownershipOutcome(ownerId, values.userId); + + const [own] = await tx + .select({ providerAccountId: core_users_sso.providerAccountId }) + .from(core_users_sso) + .where( + and( + eq(core_users_sso.userId, values.userId), + eq(core_users_sso.providerId, values.providerId), + ), + ) + .limit(1); + + if (own) return "provider_in_use"; + + await tx.insert(core_users_sso).values(values); + + return "linked"; + }); + } catch (error) { + if (!isUniqueViolation(error)) throw error; + + const ownerId = await accountOwner(db, values); + + return ownerId === null + ? "provider_in_use" + : ownershipOutcome(ownerId, values.userId); + } + }, + + listConnections: async userId => + await db + .select(connectionColumns) + .from(core_users_sso) + .where(eq(core_users_sso.userId, userId)) + .orderBy(asc(core_users_sso.createdAt)), + + listSources: async userId => + await db + .select({ + field: core_users_sso_profile_sources.field, + providerId: core_users_sso_profile_sources.providerId, + }) + .from(core_users_sso_profile_sources) + .where(eq(core_users_sso_profile_sources.userId, userId)), + + readAccount: async userId => { + const [row] = await db + .select({ + avatarId: core_users.avatarId, + email: core_users.email, + firstName: core_users.firstName, + id: core_users.id, + lastName: core_users.lastName, + name: core_users.name, + roleId: core_users.roleId, + }) + .from(core_users) + .where(eq(core_users.id, userId)) + .limit(1); + + return row ?? null; + }, + + recordAvatar: async ({ fileId, providerId, sha256, sourceUrl, userId }) => { + await db + .update(core_users_sso) + .set({ + avatarFileId: fileId, + avatarSha256: sha256, + avatarSourceUrl: sourceUrl, + }) + .where( + and( + eq(core_users_sso.userId, userId), + eq(core_users_sso.providerId, providerId), + ), + ); + }, + + recordSignIn: async ({ + providerEmail, + providerId, + providerUsername, + userId, + }) => { + await db + .update(core_users_sso) + .set({ providerEmail, providerUsername }) + .where( + and( + eq(core_users_sso.userId, userId), + eq(core_users_sso.providerId, providerId), + ), + ); + }, + + savePreferences: async ({ sources, sync, userId }) => { + try { + await db.transaction(async tx => { + for (const [providerId, syncOnSignIn] of Object.entries(sync)) { + await tx + .update(core_users_sso) + .set({ syncOnSignIn }) + .where( + and( + eq(core_users_sso.userId, userId), + eq(core_users_sso.providerId, providerId), + ), + ); + } + + for (const field of SSO_PROFILE_FIELDS) { + const providerId = sources[field]; + if (providerId === undefined) continue; + + if (providerId === null) { + await tx + .delete(core_users_sso_profile_sources) + .where( + and( + eq(core_users_sso_profile_sources.userId, userId), + eq(core_users_sso_profile_sources.field, field), + ), + ); + continue; + } + + await tx + .insert(core_users_sso_profile_sources) + .values({ field, providerId, userId }) + .onConflictDoUpdate({ + set: { createdAt: new Date(), providerId }, + target: [ + core_users_sso_profile_sources.userId, + core_users_sso_profile_sources.field, + ], + }); + } + }); + } catch (error) { + if (isForeignKeyViolation(error)) return "not_connected"; + + throw error; + } + + return "saved"; + }, + + saveOperation: async values => { + await db.insert(core_users_sso_operations).values(values); + }, + + savePreview: async values => { + await db.transaction(async tx => { + await tx + .delete(core_users_sso_operations) + .where( + and( + eq(core_users_sso_operations.userId, values.userId), + eq(core_users_sso_operations.providerId, values.providerId), + eq(core_users_sso_operations.intent, "preview"), + ), + ); + await tx.insert(core_users_sso_operations).values(values); + }); + }, + + signInFacts: async userId => { + const [[user], [passkeys]] = await Promise.all([ + db + .select({ password: core_users.password }) + .from(core_users) + .where(eq(core_users.id, userId)) + .limit(1), + db + .select({ value: count() }) + .from(core_users_passkeys) + .where(eq(core_users_passkeys.userId, userId)), + ]); + + return { + hasPassword: !!user?.password, + passkeys: passkeys?.value ?? 0, + }; + }, + + takePreview: async ({ now, providerId, userId }) => { + const [row] = await db + .delete(core_users_sso_operations) + .where(previewOf(userId, providerId, now)) + .returning(); + + return row ? toOperation(row) : null; + }, + + writeNames: async ({ userId, values }) => { + await db.update(core_users).set(values).where(eq(core_users.id, userId)); + }, +}); diff --git a/packages/vitnode/src/api/models/sso-connection.ts b/packages/vitnode/src/api/models/sso-connection.ts new file mode 100644 index 000000000..484edfb28 --- /dev/null +++ b/packages/vitnode/src/api/models/sso-connection.ts @@ -0,0 +1,812 @@ +import type { Context } from "hono"; + +import type { EnvVitNode } from "@/api/middlewares/global.middleware"; +import type { SsoConnectionIntent, SsoProfileField } from "@/lib/sso-profile"; + +import { describeError } from "@/api/lib/error-details"; +import { isPasswordSignInEnabled } from "@/api/lib/password-sign-in"; +import { resolvePersonalInfoPolicy } from "@/api/lib/personal-info-policy"; +import { hashSessionToken } from "@/api/lib/session-token"; +import { + type AvatarImportOutcome, + type AvatarImportRequest, + importProviderAvatar, +} from "@/api/lib/sso-avatar-import"; +import { + normalizeSsoProfile, + PROFILE_VALUE_KEY, + restrictProfile, + supportedProfileFields, +} from "@/api/lib/sso-profile"; +import { resolveUserImagePolicy } from "@/api/lib/user-images"; +import { CONFIG } from "@/lib/config"; +import { normalizeEmailAddress } from "@/lib/email-canonical"; +import { + SSO_CONNECTION_STATE_PREFIX, + SSO_PROFILE_FIELDS, + ssoConnectionIntentOfState, +} from "@/lib/sso-profile"; + +import type { SSOApiPlugin } from "./sso"; +import type { + SsoAccountProfile, + SsoConnectionRecord, + SsoConnectionStore, + SsoProfileValues, + SsoSignInFacts, +} from "./sso-connection-store"; + +import { invalidateSessionCacheForUser } from "./session-revoke"; +import { drizzleSsoConnectionStore } from "./sso-connection-store"; + +export const SSO_CONNECTION_STATE_TTL_MS = 10 * 60_000; + +const SSO_IMPORT_PREVIEW_TTL_MS = 10 * 60_000; + +export type SsoConnectionErrorCode = + | "account_mismatch" + | "account_taken" + | "invalid_fields" + | "invalid_source" + | "invalid_state" + | "last_sign_in_method" + | "not_connected" + | "nothing_to_sync" + | "preview_not_found" + | "provider_already_connected" + | "provider_error" + | "provider_not_found"; + +type SsoConnectionErrorStatus = 400 | 404 | 409 | 502; + +export class SsoConnectionError extends Error { + constructor(code: SsoConnectionErrorCode, status: SsoConnectionErrorStatus) { + super(code); + this.name = "SsoConnectionError"; + this.code = code; + this.status = status; + } + + readonly code: SsoConnectionErrorCode; + readonly status: SsoConnectionErrorStatus; +} + +export type SsoFieldOutcome = + "failed" | "missing" | "not_allowed" | "unchanged" | "updated"; + +export type SsoFieldOutcomes = Partial< + Record +>; + +export interface SsoProfilePolicy { + avatar: { allowed: boolean; maxBytes: number }; + firstName: boolean; + lastName: boolean; +} + +export interface SsoProviderIdentity { + email: null | string; + id: string; + profile: SsoProfileValues; + username: null | string; +} + +type SsoSyncTrigger = "import" | "sign_in" | "sync"; + +const PROVIDER_TEXT_MAX_LENGTH = 255; + +const boundedText = (value: unknown): null | string => { + if (typeof value !== "string") return null; + const trimmed = value.trim(); + + return trimmed.length > 0 && trimmed.length <= PROVIDER_TEXT_MAX_LENGTH + ? trimmed + : null; +}; + +const keepsSignInMethod = ({ + configuredProviderIds, + facts, + passkeysEnabled, + passwordEnabled, +}: { + configuredProviderIds: readonly string[]; + facts: SsoSignInFacts; + passkeysEnabled: boolean; + passwordEnabled: boolean; +}): boolean => + (passwordEnabled && facts.hasPassword) || + (passkeysEnabled && facts.passkeys > 0) || + facts.otherSsoProviderIds.some(id => configuredProviderIds.includes(id)); + +const randomState = (intent: SsoConnectionIntent): string => { + const bytes = crypto.getRandomValues(new Uint8Array(32)); + + return `${SSO_CONNECTION_STATE_PREFIX[intent]}${Array.from(bytes, byte => + byte.toString(16).padStart(2, "0"), + ).join("")}`; +}; + +const uniqueFields = (fields: readonly SsoProfileField[]): SsoProfileField[] => + SSO_PROFILE_FIELDS.filter(field => fields.includes(field)); + +const toPublicConnection = (connection: SsoConnectionRecord) => ({ + accountLabel: connection.providerEmail ?? connection.providerUsername, + connectedAt: connection.createdAt, + email: connection.providerEmail, + syncOnSignIn: connection.syncOnSignIn, +}); + +export class SsoConnectionModel { + constructor(c: Context) { + this.c = c; + } + + protected readonly c: Context; + + private adapter(providerId: string): SSOApiPlugin { + const adapter = this.adapters.find(one => one.id === providerId); + if (!adapter) throw new SsoConnectionError("provider_not_found", 404); + + return adapter; + } + + private async applyAvatar({ + account, + connection, + policy, + profile, + }: { + account: SsoAccountProfile; + connection: SsoConnectionRecord; + policy: SsoProfilePolicy; + profile: SsoProfileValues; + }): Promise { + if (!profile.avatarUrl) return "missing"; + if (!policy.avatar.allowed) return "not_allowed"; + + try { + const outcome = await this.importAvatar({ + currentAvatarId: account.avatarId, + maxBytes: policy.avatar.maxBytes, + previous: { + fileId: connection.avatarFileId, + sha256: connection.avatarSha256, + sourceUrl: connection.avatarSourceUrl, + }, + url: profile.avatarUrl, + userId: account.id, + }); + + if (outcome.status === "unchanged") return "unchanged"; + + await this.store.recordAvatar({ + fileId: outcome.fileId, + providerId: connection.providerId, + sha256: outcome.sha256, + sourceUrl: outcome.sourceUrl, + userId: account.id, + }); + + return "updated"; + } catch (error) { + this.warn("avatar import", error); + + return "failed"; + } + } + + private async applyProfile({ + connection, + fields, + profile, + trigger, + }: { + connection: SsoConnectionRecord; + fields: readonly SsoProfileField[]; + profile: SsoProfileValues; + trigger: SsoSyncTrigger; + }): Promise { + const account = await this.store.readAccount(connection.userId); + if (!account) throw new SsoConnectionError("not_connected", 404); + + const policy = await this.profilePolicy(account); + const results: SsoFieldOutcomes = {}; + const names: Partial> = {}; + + for (const field of ["firstName", "lastName"] as const) { + if (!fields.includes(field)) continue; + + const value = profile[field]; + if (!value) { + results[field] = "missing"; + } else if (!policy[field]) { + results[field] = "not_allowed"; + } else if (account[field] === value) { + results[field] = "unchanged"; + } else { + names[field] = value; + results[field] = "updated"; + } + } + + if (Object.keys(names).length > 0) { + await this.store.writeNames({ userId: account.id, values: names }); + await this.refreshSessions(account.id); + await this.c.get("events").emit("user.updated", { + email: account.email, + name: account.name, + userId: account.id, + }); + } + + if (fields.includes("avatar")) { + results.avatar = await this.applyAvatar({ + account, + connection, + policy, + profile, + }); + } + + const updated = SSO_PROFILE_FIELDS.filter( + field => results[field] === "updated", + ); + if (updated.length > 0) { + await this.c.get("events").emit("user.sso.profile_synced", { + fields: updated, + providerId: connection.providerId, + trigger, + userId: account.id, + }); + } + + return results; + } + + private async fetchIdentity( + adapter: SSOApiPlugin, + code: string, + ): Promise { + let user: Awaited>; + try { + user = await adapter.fetchUser(await adapter.fetchToken(code)); + } catch (error) { + this.warn("provider request", error); + throw new SsoConnectionError("provider_error", 502); + } + + const id = boundedText(user.id); + if (!id) throw new SsoConnectionError("provider_error", 502); + + return ssoIdentityOf(adapter, { ...user, id }); + } + + private async sourcedFields({ + adapter, + providerId, + userId, + }: { + adapter: SSOApiPlugin; + providerId: string; + userId: number; + }): Promise { + const supported = supportedProfileFields(adapter); + const sources = await this.store.listSources(userId); + + return uniqueFields( + sources + .filter( + source => + source.providerId === providerId && + supported.includes(source.field), + ) + .map(source => source.field), + ); + } + + private warn(step: string, error: unknown) { + if (!CONFIG.node_development) return; + + // eslint-disable-next-line no-console + console.warn( + `\x1b[34m[VitNode]\x1b[0m \x1b[33mSSO connection ${step} failed:\x1b[0m ${describeError(error)}`, + ); + } + + async afterSignIn({ + identity, + providerId, + userId, + }: { + identity: SsoProviderIdentity; + providerId: string; + userId: number; + }): Promise { + try { + await this.store.recordSignIn({ + providerEmail: identity.email, + providerId, + providerUsername: identity.username, + userId, + }); + + const connection = await this.store.findConnection({ + providerId, + userId, + }); + if (!connection?.syncOnSignIn) return; + + const fields = await this.sourcedFields({ + adapter: this.adapter(providerId), + providerId, + userId, + }); + if (fields.length === 0) return; + + await this.applyProfile({ + connection, + fields, + profile: identity.profile, + trigger: "sign_in", + }); + } catch (error) { + this.warn("sign-in sync", error); + } + } + + async applyImport({ + fields, + providerId, + userId, + }: { + fields: readonly SsoProfileField[]; + providerId: string; + userId: number; + }): Promise<{ manualFields: SsoProfileField[]; results: SsoFieldOutcomes }> { + const selected = uniqueFields(fields); + const preview = await this.store.takePreview({ + now: new Date(), + providerId, + userId, + }); + + if (!preview?.preview) { + throw new SsoConnectionError("preview_not_found", 404); + } + if ( + selected.length === 0 || + selected.some(field => !preview.fields.includes(field)) + ) { + throw new SsoConnectionError("invalid_fields", 400); + } + + const connection = await this.store.findConnection({ providerId, userId }); + if (!connection) throw new SsoConnectionError("not_connected", 404); + if (connection.providerAccountId !== preview.providerAccountId) { + throw new SsoConnectionError("account_mismatch", 409); + } + + const results = await this.applyProfile({ + connection, + fields: selected, + profile: preview.preview, + trigger: "import", + }); + + const sourcedElsewhere = (await this.store.listSources(userId)) + .filter( + source => + source.providerId !== providerId && + results[source.field] === "updated", + ) + .map(source => source.field); + const manualFields = await this.store.clearSources({ + fields: sourcedElsewhere, + userId, + }); + + return { manualFields, results }; + } + + async authorize({ + fields = [], + intent, + providerId, + userId, + }: { + fields?: readonly SsoProfileField[]; + intent: SsoConnectionIntent; + providerId: string; + userId: number; + }): Promise<{ url: string }> { + const adapter = this.adapter(providerId); + const now = new Date(); + await this.store.deleteExpiredOperations(now); + + const connection = await this.store.findConnection({ providerId, userId }); + let bound: SsoProfileField[] = []; + + if (intent === "link") { + if (connection) { + throw new SsoConnectionError("provider_already_connected", 409); + } + } else if (intent === "sync") { + if (!connection) throw new SsoConnectionError("not_connected", 404); + + bound = await this.sourcedFields({ adapter, providerId, userId }); + if (bound.length === 0) { + throw new SsoConnectionError("nothing_to_sync", 400); + } + } else { + if (!connection) throw new SsoConnectionError("not_connected", 404); + + const supported = supportedProfileFields(adapter); + bound = uniqueFields(fields); + if ( + bound.length === 0 || + bound.some(field => !supported.includes(field)) + ) { + throw new SsoConnectionError("invalid_fields", 400); + } + } + + const state = randomState(intent); + await this.store.saveOperation({ + expiresAt: new Date(now.getTime() + SSO_CONNECTION_STATE_TTL_MS), + fields: bound, + intent, + preview: null, + providerAccountId: connection?.providerAccountId ?? null, + providerId, + tokenHash: await hashSessionToken(state), + userId, + }); + + return { url: adapter.getUrl({ state }) }; + } + + async callback({ + code, + providerId, + state, + user, + }: { + code: string; + providerId: string; + state: string; + user: { email: string; id: number }; + }): Promise<{ + intent: SsoConnectionIntent; + providerId: string; + results?: SsoFieldOutcomes; + }> { + const intent = ssoConnectionIntentOfState(state); + if (!intent) throw new SsoConnectionError("invalid_state", 400); + + const operation = await this.store.consumeOperation( + await hashSessionToken(state), + ); + const now = new Date(); + + if ( + operation?.intent !== intent || + operation.expiresAt <= now || + operation.providerId !== providerId || + operation.userId !== user.id + ) { + throw new SsoConnectionError("invalid_state", 400); + } + + const adapter = this.adapter(providerId); + const identity = await this.fetchIdentity(adapter, code); + + if (intent === "link") { + const outcome = await this.store.link({ + providerAccountId: identity.id, + providerEmail: identity.email, + providerId, + providerUsername: identity.username, + userId: user.id, + }); + + if (outcome === "taken") { + throw new SsoConnectionError("account_taken", 409); + } + if (outcome === "provider_in_use") { + throw new SsoConnectionError("provider_already_connected", 409); + } + if (outcome === "linked") { + await this.c.get("events").emit("user.sso.linked", { + email: user.email, + providerId, + userId: user.id, + }); + } + + return { intent, providerId }; + } + + const connection = await this.store.findConnection({ + providerId, + userId: user.id, + }); + if (!connection) throw new SsoConnectionError("not_connected", 404); + if (connection.providerAccountId !== identity.id) { + throw new SsoConnectionError("account_mismatch", 409); + } + + await this.store.recordSignIn({ + providerEmail: identity.email, + providerId, + providerUsername: identity.username, + userId: user.id, + }); + + if (intent === "sync") { + const stillSourced = await this.sourcedFields({ + adapter, + providerId, + userId: user.id, + }); + const fields = operation.fields.filter(field => + stillSourced.includes(field), + ); + const results = + fields.length > 0 + ? await this.applyProfile({ + connection, + fields, + profile: identity.profile, + trigger: "sync", + }) + : {}; + + return { intent, providerId, results }; + } + + await this.store.savePreview({ + expiresAt: new Date(now.getTime() + SSO_IMPORT_PREVIEW_TTL_MS), + fields: operation.fields, + intent: "preview", + preview: restrictProfile(identity.profile, operation.fields), + providerAccountId: identity.id, + providerId, + tokenHash: null, + userId: user.id, + }); + + return { intent, providerId }; + } + + async clearSourcesAfterManualEdit({ + fields, + userId, + }: { + fields: readonly SsoProfileField[]; + userId: number; + }): Promise { + return await this.store.clearSources({ + fields: uniqueFields(fields), + userId, + }); + } + + async discardPreview({ + providerId, + userId, + }: { + providerId: string; + userId: number; + }): Promise { + await this.store.takePreview({ now: new Date(), providerId, userId }); + } + + async disconnect({ + providerId, + userId, + }: { + providerId: string; + userId: number; + }): Promise { + const configuredProviderIds = this.adapters.map(adapter => adapter.id); + const passwordEnabled = isPasswordSignInEnabled(this.c); + const passkeysEnabled = this.c.get("core").authorization.passkeys.enabled; + + const outcome = await this.store.disconnect({ + canDisconnect: facts => + keepsSignInMethod({ + configuredProviderIds, + facts, + passkeysEnabled, + passwordEnabled, + }), + providerId, + userId, + }); + + if (outcome === "not_found") { + throw new SsoConnectionError("not_connected", 404); + } + if (outcome === "blocked") { + throw new SsoConnectionError("last_sign_in_method", 409); + } + + await this.c + .get("events") + .emit("user.sso.unlinked", { providerId, userId }); + } + + async importAvatar( + request: AvatarImportRequest, + ): Promise { + return await importProviderAvatar(this.c, request); + } + + async overview(userId: number) { + const [connections, sources, facts] = await Promise.all([ + this.store.listConnections(userId), + this.store.listSources(userId), + this.store.signInFacts(userId), + ]); + const passwordEnabled = isPasswordSignInEnabled(this.c); + const passkeysEnabled = this.c.get("core").authorization.passkeys.enabled; + const sourceOf = (field: SsoProfileField) => + sources.find(source => source.field === field)?.providerId ?? null; + + const configured = this.adapters.map(adapter => ({ + available: true, + icon: adapter.icon ?? null, + id: adapter.id, + name: adapter.name, + profileFields: supportedProfileFields(adapter), + })); + const orphaned = connections + .filter( + connection => !configured.some(one => one.id === connection.providerId), + ) + .map(connection => ({ + available: false, + icon: null, + id: connection.providerId, + name: connection.providerId, + profileFields: [], + })); + + return { + providers: [...configured, ...orphaned].map(provider => { + const connection = connections.find( + one => one.providerId === provider.id, + ); + + return { + ...provider, + connection: connection ? toPublicConnection(connection) : null, + }; + }), + signIn: { + hasPassword: passwordEnabled && facts.hasPassword, + passkeys: passkeysEnabled ? facts.passkeys : 0, + passkeysEnabled, + passwordEnabled, + }, + sources: { + avatar: sourceOf("avatar"), + firstName: sourceOf("firstName"), + lastName: sourceOf("lastName"), + }, + }; + } + + async preview({ + providerId, + userId, + }: { + providerId: string; + userId: number; + }) { + const operation = await this.store.findPreview({ + now: new Date(), + providerId, + userId, + }); + const account = await this.store.readAccount(userId); + if (!(operation?.preview && account)) { + throw new SsoConnectionError("preview_not_found", 404); + } + + const policy = await this.profilePolicy(account); + const sources = await this.store.listSources(userId); + const { preview } = operation; + + return { + expiresAt: operation.expiresAt, + fields: operation.fields.map(field => ({ + allowed: field === "avatar" ? policy.avatar.allowed : policy[field], + current: field === "avatar" ? null : account[field], + field, + incoming: preview[PROFILE_VALUE_KEY[field]], + source: + sources.find(source => source.field === field)?.providerId ?? null, + })), + }; + } + + async profilePolicy(account: { + id: number; + roleId: number; + }): Promise { + const [personal, images] = await Promise.all([ + resolvePersonalInfoPolicy(this.c, account), + resolveUserImagePolicy(this.c, account), + ]); + + return { + avatar: images.avatar, + firstName: personal.canEdit && personal.fields.firstName, + lastName: personal.canEdit && personal.fields.lastName, + }; + } + + async refreshSessions(userId: number): Promise { + await invalidateSessionCacheForUser(this.c, userId); + } + + async savePreferences({ + sources, + sync, + userId, + }: { + sources: Partial>; + sync: Record; + userId: number; + }): Promise { + const connections = await this.store.listConnections(userId); + const connected = new Set(connections.map(one => one.providerId)); + + for (const field of SSO_PROFILE_FIELDS) { + const providerId = sources[field]; + if (!providerId) continue; + + const adapter = this.adapters.find(one => one.id === providerId); + if (!( + connected.has(providerId) && + adapter && + supportedProfileFields(adapter).includes(field) + )) { + throw new SsoConnectionError("invalid_source", 400); + } + } + + if (Object.keys(sync).some(providerId => !connected.has(providerId))) { + throw new SsoConnectionError("invalid_source", 400); + } + + const outcome = await this.store.savePreferences({ sources, sync, userId }); + if (outcome === "not_connected") { + throw new SsoConnectionError("not_connected", 409); + } + } + + private get adapters(): SSOApiPlugin[] { + return this.c.get("core").authorization.ssoAdapters; + } + + get store(): SsoConnectionStore { + return drizzleSsoConnectionStore(this.c.get("db")); + } +} + +export const ssoIdentityOf = ( + adapter: Pick, + user: Awaited>, +): SsoProviderIdentity => { + const email = boundedText(user.email); + + return { + email: email ? normalizeEmailAddress(email) : null, + id: user.id, + profile: normalizeSsoProfile(adapter, user), + username: boundedText(user.username), + }; +}; diff --git a/packages/vitnode/src/api/models/sso.ts b/packages/vitnode/src/api/models/sso.ts index 926c39417..45bdad774 100644 --- a/packages/vitnode/src/api/models/sso.ts +++ b/packages/vitnode/src/api/models/sso.ts @@ -5,7 +5,11 @@ import { getCookie } from "hono/cookie"; import { HTTPException } from "hono/http-exception"; import crypto from "node:crypto"; +import type { EnvVitNode } from "@/api/middlewares/global.middleware"; +import type { SsoProfileField } from "@/lib/sso-profile"; + import { deleteAuthCookie, setAuthCookie } from "@/api/lib/auth-cookie"; +import { isUniqueViolation } from "@/api/lib/db-errors"; import { isPasswordSignInEnabled } from "@/api/lib/password-sign-in"; import { ensureServerSecret } from "@/api/lib/server-secret"; import { ssoConfirmsEmail } from "@/api/lib/sso-email-confirmation"; @@ -20,6 +24,7 @@ import { normalizeEmailAddress } from "@/lib/email-canonical"; import { removeSpecialCharacters } from "@/lib/special-characters"; import { PasswordModel } from "./password"; +import { SsoConnectionModel, ssoIdentityOf } from "./sso-connection"; import { createSsoLinkToken, SSO_LINK_SECRET_NAME, @@ -27,6 +32,15 @@ import { } from "./sso-link-token"; import { UserModel } from "./user"; +export interface SSOProviderUser { + avatarUrl?: null | string; + email: string; + firstName?: null | string; + id: string; + lastName?: null | string; + username: string; +} + export interface SSOApiPlugin { fetchToken: ( code: string, @@ -34,11 +48,12 @@ export interface SSOApiPlugin { fetchUser: (args: { access_token: string; token_type: string; - }) => Promise<{ email: string; id: string; username: string }>; + }) => Promise; getUrl: (props: { state: string }) => string; icon?: string; id: string; name: string; + profileFields?: readonly SsoProfileField[]; } export type SSOCallbackOutcome = @@ -54,12 +69,12 @@ export const getRedirectUri = (code: string) => new URL(`${CONFIG.web.href}login/sso/${code}`).toString(); export class SSOModel { - constructor(c: Context) { + constructor(c: Context) { this.c = c; this.plugins = c.get("core").authorization.ssoAdapters; } - private readonly c: Context; + private readonly c: Context; private readonly plugins: SSOApiPlugin[]; private readonly signUpUser = async ({ @@ -85,11 +100,16 @@ export class SSOModel { }, c, ); - await c.get("db").insert(core_users_sso).values({ - userId: data.id, - providerId: providerId, - providerAccountId: user.id, - }); + await c + .get("db") + .insert(core_users_sso) + .values({ + userId: data.id, + providerId: providerId, + providerAccountId: user.id, + providerEmail: user.email, + providerUsername: user.username.slice(0, 255), + }); return { userId: data.id }; }; @@ -125,11 +145,12 @@ export class SSOModel { const ssoToken = await provider.fetchToken(code); const userFromProvider = await provider.fetchUser(ssoToken); const userFromSSO = { - ...userFromProvider, email: normalizeEmailAddress(userFromProvider.email), + id: userFromProvider.id, + username: userFromProvider.username, }; - return await this.c.get("db").transaction(async tx => { + const outcome = await this.c.get("db").transaction(async tx => { const [dataSSOFromDb] = await tx .select({ userId: core_users_sso.userId, @@ -168,14 +189,14 @@ export class SSOModel { c: this.c, }); - return { kind: "signed_in", userId: signUpUser.userId }; + return { kind: "signed_in", userId: signUpUser.userId } as const; } return { email: userWithEmail.email, hasPassword: isPasswordSignInEnabled(this.c) && userWithEmail.password !== null, - kind: "link_required", + kind: "link_required" as const, linkToken: await this.mintLinkToken({ email: userFromSSO.email, providerAccountId: userFromSSO.id, @@ -198,8 +219,20 @@ export class SSOModel { .where(eq(core_users.id, dataSSOFromDb.userId)); } - return { kind: "signed_in", userId: dataSSOFromDb.userId }; + return { kind: "returning", userId: dataSSOFromDb.userId } as const; }); + + if (outcome.kind === "returning") { + await new SsoConnectionModel(this.c).afterSignIn({ + identity: ssoIdentityOf(provider, userFromProvider), + providerId, + userId: outcome.userId, + }); + + return { kind: "signed_in", userId: outcome.userId }; + } + + return outcome; } async encryptState() { @@ -279,45 +312,74 @@ export class SSOModel { throw new HTTPException(403); } - await this.c.get("db").transaction(async tx => { - const [existing] = await tx - .select({ userId: core_users_sso.userId }) - .from(core_users_sso) - .where( - and( - eq(core_users_sso.providerId, providerId), - eq(core_users_sso.providerAccountId, offer.providerAccountId), - ), - ) - .limit(1); + await this.c + .get("db") + .transaction(async tx => { + const [existing] = await tx + .select({ userId: core_users_sso.userId }) + .from(core_users_sso) + .where( + and( + eq(core_users_sso.providerId, providerId), + eq(core_users_sso.providerAccountId, offer.providerAccountId), + ), + ) + .limit(1); + + if (existing && existing.userId !== user.id) { + throw new HTTPException(409, { + message: "Provider account already linked", + }); + } - if (existing && existing.userId !== user.id) { - throw new HTTPException(409, { - message: "Provider account already linked", - }); - } + if (!existing) { + const [otherAccount] = await tx + .select({ userId: core_users_sso.userId }) + .from(core_users_sso) + .where( + and( + eq(core_users_sso.userId, user.id), + eq(core_users_sso.providerId, providerId), + ), + ) + .limit(1); + + if (otherAccount) { + throw new HTTPException(409, { + message: "Another account from this provider is already linked", + }); + } + + await tx.insert(core_users_sso).values({ + userId: user.id, + providerId, + providerAccountId: offer.providerAccountId, + providerEmail: offer.email, + }); + } - if (!existing) { - await tx.insert(core_users_sso).values({ - userId: user.id, - providerId, - providerAccountId: offer.providerAccountId, - }); - } + if ( + ssoConfirmsEmail({ + accountEmail: user.email, + emailVerified: user.emailVerified, + providerEmail: offer.email, + }) + ) { + await tx + .update(core_users) + .set({ emailVerified: true }) + .where(eq(core_users.id, user.id)); + } + }) + .catch((error: unknown) => { + if (isUniqueViolation(error)) { + throw new HTTPException(409, { + message: "Provider account already linked", + }); + } - if ( - ssoConfirmsEmail({ - accountEmail: user.email, - emailVerified: user.emailVerified, - providerEmail: offer.email, - }) - ) { - await tx - .update(core_users) - .set({ emailVerified: true }) - .where(eq(core_users.id, user.id)); - } - }); + throw error; + }); await this.c.get("events").emit("user.sso.linked", { email: user.email, diff --git a/packages/vitnode/src/api/modules/admin/users/routes/image-delete.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/image-delete.route.ts index dfeb69c8a..64b3651bb 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/image-delete.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/image-delete.route.ts @@ -3,6 +3,7 @@ import { eq } from "drizzle-orm"; import { buildRoute } from "@/api/lib/route"; import { removeUserImage, zodUserImageKind } from "@/api/lib/user-images"; +import { SsoConnectionModel } from "@/api/models/sso-connection"; import { CONFIG_PLUGIN } from "@/config"; import { core_users } from "@/database/users"; @@ -58,6 +59,12 @@ export const deleteUserImageAdminRoute = buildRoute({ } await assertCanEditAdminTarget(c, userId); + if (kind === "avatar") { + await new SsoConnectionModel(c).clearSourcesAfterManualEdit({ + fields: ["avatar"], + userId: user.id, + }); + } await removeUserImage(c, { kind, userId: user.id }); return c.body(null, 200); diff --git a/packages/vitnode/src/api/modules/admin/users/routes/image-upload.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/image-upload.route.ts index 78002320e..cb9a2316c 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/image-upload.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/image-upload.route.ts @@ -8,6 +8,7 @@ import { setUserImage, zodUserImageKind, } from "@/api/lib/user-images"; +import { SsoConnectionModel } from "@/api/models/sso-connection"; import { CONFIG_PLUGIN } from "@/config"; import { core_users } from "@/database/users"; @@ -93,6 +94,13 @@ export const uploadUserImageAdminRoute = buildRoute({ const policy = await resolveUserImagePolicy(c, user, { ignoreAllow: true }); + if (kind === "avatar") { + await new SsoConnectionModel(c).clearSourcesAfterManualEdit({ + fields: ["avatar"], + userId: user.id, + }); + } + try { const stored = await setUserImage(c, { file, @@ -101,7 +109,7 @@ export const uploadUserImageAdminRoute = buildRoute({ userId: user.id, }); - return c.json(stored, 200); + return c.json({ url: stored.url }, 200); } catch (error) { if (error instanceof HTTPException && error.status === 400) { return c.json({ error: error.message }, 400); diff --git a/packages/vitnode/src/api/modules/users/images/routes/delete.route.ts b/packages/vitnode/src/api/modules/users/images/routes/delete.route.ts index a0fe8a845..f942e2cc9 100644 --- a/packages/vitnode/src/api/modules/users/images/routes/delete.route.ts +++ b/packages/vitnode/src/api/modules/users/images/routes/delete.route.ts @@ -3,6 +3,7 @@ import { HTTPException } from "hono/http-exception"; import { buildRoute } from "@/api/lib/route"; import { removeUserImage, zodUserImageKind } from "@/api/lib/user-images"; +import { SsoConnectionModel } from "@/api/models/sso-connection"; import { CONFIG_PLUGIN } from "@/config"; export const deleteUserImageRoute = buildRoute({ @@ -32,6 +33,12 @@ export const deleteUserImageRoute = buildRoute({ } const { kind } = c.req.valid("param"); + if (kind === "avatar") { + await new SsoConnectionModel(c).clearSourcesAfterManualEdit({ + fields: ["avatar"], + userId: user.id, + }); + } await removeUserImage(c, { kind, userId: user.id }); return c.body(null, 200); diff --git a/packages/vitnode/src/api/modules/users/images/routes/upload.route.ts b/packages/vitnode/src/api/modules/users/images/routes/upload.route.ts index ee3187148..abf974cec 100644 --- a/packages/vitnode/src/api/modules/users/images/routes/upload.route.ts +++ b/packages/vitnode/src/api/modules/users/images/routes/upload.route.ts @@ -7,6 +7,7 @@ import { setUserImage, zodUserImageKind, } from "@/api/lib/user-images"; +import { SsoConnectionModel } from "@/api/models/sso-connection"; import { CONFIG_PLUGIN } from "@/config"; export const uploadUserImageRoute = buildRoute({ @@ -80,6 +81,13 @@ export const uploadUserImageRoute = buildRoute({ ); } + if (kind === "avatar") { + await new SsoConnectionModel(c).clearSourcesAfterManualEdit({ + fields: ["avatar"], + userId: user.id, + }); + } + try { const stored = await setUserImage(c, { file, @@ -88,7 +96,7 @@ export const uploadUserImageRoute = buildRoute({ userId: user.id, }); - return c.json(stored, 200); + return c.json({ url: stored.url }, 200); } catch (error) { if (error instanceof HTTPException && error.status === 400) { return c.json({ error: error.message }, 400); diff --git a/packages/vitnode/src/api/modules/users/routes/update-me.route.ts b/packages/vitnode/src/api/modules/users/routes/update-me.route.ts index a8a1d0f11..154594b0f 100644 --- a/packages/vitnode/src/api/modules/users/routes/update-me.route.ts +++ b/packages/vitnode/src/api/modules/users/routes/update-me.route.ts @@ -5,6 +5,7 @@ import { HTTPException } from "hono/http-exception"; import { resolvePersonalInfoPolicy } from "@/api/lib/personal-info-policy"; import { buildRoute } from "@/api/lib/route"; import { invalidateSessionCacheForUser } from "@/api/models/session-revoke"; +import { SsoConnectionModel } from "@/api/models/sso-connection"; import { CONFIG_PLUGIN } from "@/config"; import { core_users } from "@/database/users"; import { @@ -147,6 +148,12 @@ export const updateMeRoute = buildRoute({ throw new HTTPException(401, { message: "Unauthorized" }); } + await new SsoConnectionModel(c).clearSourcesAfterManualEdit({ + fields: (["firstName", "lastName"] as const).filter( + field => field in values, + ), + userId: user.id, + }); await invalidateSessionCacheForUser(c, user.id); await c.get("events").emit("user.updated", { userId: user.id, diff --git a/packages/vitnode/src/api/modules/users/sso/connections/connections.module.ts b/packages/vitnode/src/api/modules/users/sso/connections/connections.module.ts new file mode 100644 index 000000000..1277085b2 --- /dev/null +++ b/packages/vitnode/src/api/modules/users/sso/connections/connections.module.ts @@ -0,0 +1,28 @@ +import { buildModule } from "@/api/lib/module"; +import { CONFIG_PLUGIN } from "@/config"; + +import { authorizeSsoConnectionRoute } from "./routes/authorize.route"; +import { ssoConnectionCallbackRoute } from "./routes/callback.route"; +import { disconnectSsoConnectionRoute } from "./routes/disconnect.route"; +import { + ssoApplyImportRoute, + ssoDiscardImportRoute, + ssoImportPreviewRoute, +} from "./routes/import.route"; +import { listSsoConnectionsRoute } from "./routes/list.route"; +import { ssoPreferencesRoute } from "./routes/preferences.route"; + +export const ssoConnectionsModule = buildModule({ + pluginId: CONFIG_PLUGIN.pluginId, + name: "connections", + routes: [ + listSsoConnectionsRoute, + ssoPreferencesRoute, + authorizeSsoConnectionRoute, + ssoConnectionCallbackRoute, + ssoImportPreviewRoute, + ssoApplyImportRoute, + ssoDiscardImportRoute, + disconnectSsoConnectionRoute, + ], +}); diff --git a/packages/vitnode/src/api/modules/users/sso/connections/connections.test.ts b/packages/vitnode/src/api/modules/users/sso/connections/connections.test.ts new file mode 100644 index 000000000..e6f841532 --- /dev/null +++ b/packages/vitnode/src/api/modules/users/sso/connections/connections.test.ts @@ -0,0 +1,1177 @@ +// @vitest-environment node +import type { Context } from "hono"; + +import { OpenAPIHono } from "@hono/zod-openapi"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import type { + EnvVariablesVitNode, + EnvVitNode, +} from "@/api/middlewares/global.middleware"; +import type { SSOApiPlugin, SSOProviderUser } from "@/api/models/sso"; +import type { SsoProfilePolicy } from "@/api/models/sso-connection"; + +import { SessionModel } from "@/api/models/session"; +import { SessionAdminModel } from "@/api/models/session-admin"; +import { + SSO_CONNECTION_STATE_TTL_MS, + SsoConnectionModel, + ssoIdentityOf, +} from "@/api/models/sso-connection"; +import { + createMemorySsoConnectionStore, + type MemorySsoAccount, +} from "@/tests/sso-connection-store"; + +import { ssoConnectionsModule } from "./connections.module"; + +type Authorization = EnvVariablesVitNode["core"]["authorization"]; + +const account = ( + id: number, + overrides: Partial = {}, +): MemorySsoAccount => ({ + avatarId: null, + email: `user${id}@vitnode.test`, + firstName: null, + hasPassword: true, + id, + lastName: null, + name: `User${id}`, + passkeys: 0, + roleId: 2, + showRealName: false, + ...overrides, +}); + +const ALICE = account(1, { firstName: "Alicja", lastName: "Kowalska" }); +const BOB = account(2); + +const provider = ( + id: string, + name: string, + profileFields: SSOApiPlugin["profileFields"], + users: Map, +): SSOApiPlugin & { + fetchToken: ReturnType; + fetchUser: ReturnType; +} => ({ + fetchToken: vi.fn(async (code: string) => + Promise.resolve({ access_token: code, token_type: "Bearer" }), + ), + fetchUser: vi.fn(async ({ access_token }: { access_token: string }) => { + const user = users.get(access_token); + if (!user) throw new Error("unknown code"); + + return Promise.resolve(user); + }), + getUrl: ({ state }) => + `https://${id}.example/oauth?state=${encodeURIComponent(state)}`, + id, + name, + profileFields, +}); + +const OPEN_POLICY: SsoProfilePolicy = { + avatar: { allowed: true, maxBytes: 1024 * 1024 }, + firstName: true, + lastName: true, +}; + +const json = (body: unknown, method = "POST"): RequestInit => ({ + body: JSON.stringify(body), + headers: { "content-type": "application/json" }, + method, +}); + +const harness = ({ + accounts = [ALICE, BOB], + passkeysEnabled = false, + passwordEnabled = true, +}: { + accounts?: MemorySsoAccount[]; + passkeysEnabled?: boolean; + passwordEnabled?: boolean; +} = {}) => { + const memory = createMemorySsoConnectionStore(accounts); + const profiles = new Map(); + const google = provider( + "google", + "Google", + ["avatar", "firstName", "lastName"], + profiles, + ); + const discord = provider("discord", "Discord", ["avatar"], profiles); + let adapters: SSOApiPlugin[] = [google, discord]; + let policy = OPEN_POLICY; + + vi.spyOn(SsoConnectionModel.prototype, "store", "get").mockReturnValue( + memory.store, + ); + vi.spyOn(SsoConnectionModel.prototype, "profilePolicy").mockImplementation( + async () => Promise.resolve(policy), + ); + vi.spyOn(SsoConnectionModel.prototype, "refreshSessions").mockResolvedValue( + undefined, + ); + const importAvatar = vi + .spyOn(SsoConnectionModel.prototype, "importAvatar") + .mockImplementation(async ({ url }) => + Promise.resolve({ + fileId: 500, + sha256: "a".repeat(64), + sourceUrl: url, + status: "updated" as const, + }), + ); + const createSession = vi + .spyOn(SessionModel.prototype, "createSessionByUserId") + .mockResolvedValue({ token: "session-token" }); + const createAdminSession = vi + .spyOn(SessionAdminModel.prototype, "createSessionByUserId") + .mockResolvedValue({ token: "admin-token" }); + const emit = vi.fn(async () => Promise.resolve(undefined)); + + const authorization = (): Authorization => ({ + adminCookieExpires: 1000 * 60 * 60 * 24, + adminCookieName: "vitnode_auth_admin", + cookieDomain: undefined, + cookie_expires: 1000 * 60 * 60 * 24 * 90, + cookieName: "vitnode_auth", + cookieSecure: true, + deviceCookieExpires: 1000 * 60 * 60 * 24 * 365, + deviceCookieName: "vitnode_device", + passkeys: passkeysEnabled + ? { + enabled: true, + origins: ["https://vitnode.test"], + rpId: "vitnode.test", + rpName: "VitNode", + } + : { enabled: false, problems: [] }, + password: { enabled: passwordEnabled }, + ssoAdapters: adapters, + }); + + let viewer: MemorySsoAccount | null = null; + const app = new OpenAPIHono(); + app.use("*", async (c, next) => { + c.set("core", { + authorization: authorization(), + } as EnvVariablesVitNode["core"]); + c.set("user", viewer as unknown as Context["var"]["user"]); + c.set("events", { emit } as unknown as Context["var"]["events"]); + await next(); + }); + app.route("/", ssoConnectionsModule.hono); + + const setCookies: string[] = []; + const request = async (path: string, init: RequestInit = {}) => { + const response = await app.request(path, init); + setCookies.push(...response.headers.getSetCookie()); + + return response; + }; + + const context = () => + ({ + get: (key: string) => + ({ + core: { authorization: authorization() }, + events: { emit }, + })[key], + }) as unknown as Context; + + const startRoundTrip = async ( + providerId: string, + body: { fields?: string[]; intent: "import" | "link" | "sync" }, + ) => { + const response = await request(`/${providerId}/authorize`, json(body)); + expect(response.status).toBe(200); + const { url } = (await response.json()) as { url: string }; + + return new URL(url).searchParams.get("state") ?? ""; + }; + + const finish = async (providerId: string, code: string, state: string) => + await request(`/${providerId}/callback`, json({ code, state })); + + return { + ...memory, + context, + createAdminSession, + createSession, + discord, + emit, + finish, + google, + importAvatar, + profiles, + request, + setAdapters: (next: SSOApiPlugin[]) => { + adapters = next; + }, + setCookies, + setPolicy: (next: SsoProfilePolicy) => { + policy = next; + }, + signInAs: (user: MemorySsoAccount | null) => { + viewer = user; + }, + startRoundTrip, + }; +}; + +type Harness = ReturnType; + +const connectGoogle = async ( + h: Harness, + profile: Partial = {}, +) => { + h.profiles.set("code-google", { + email: "alice.personal@gmail.test", + id: "google-alice", + username: "Alice Personal", + ...profile, + }); + const state = await h.startRoundTrip("google", { intent: "link" }); + + return await h.finish("google", "code-google", state); +}; + +const errorOf = async (response: Response) => + ((await response.json()) as { error: string }).error; + +afterEach(() => { + vi.restoreAllMocks(); + vi.useRealTimers(); +}); + +describe("connecting an account from settings", () => { + it("links the provider account to the signed-in user even when its email differs", async () => { + const h = harness(); + h.signInAs(ALICE); + + const response = await connectGoogle(h); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ + intent: "link", + providerId: "google", + }); + expect(h.connections).toEqual([ + expect.objectContaining({ + providerAccountId: "google-alice", + providerEmail: "alice.personal@gmail.test", + providerId: "google", + syncOnSignIn: false, + userId: ALICE.id, + }), + ]); + expect(h.users.get(ALICE.id)?.email).toBe(ALICE.email); + expect(h.emit).toHaveBeenCalledWith("user.sso.linked", { + email: ALICE.email, + providerId: "google", + userId: ALICE.id, + }); + }); + + it("never signs anybody in, and never opens an AdminCP session", async () => { + const h = harness(); + h.signInAs(ALICE); + + await connectGoogle(h); + + expect(h.createSession).not.toHaveBeenCalled(); + expect(h.createAdminSession).not.toHaveBeenCalled(); + expect(h.setCookies.join(";")).not.toContain("vitnode_auth_admin"); + }); + + it("does not copy anything onto the profile just because an account was connected", async () => { + const h = harness(); + h.signInAs(ALICE); + + await connectGoogle(h, { + avatarUrl: "https://lh3.googleusercontent.test/a/photo.png", + firstName: "Somebody", + lastName: "Else", + }); + + expect(h.users.get(ALICE.id)).toMatchObject({ + avatarId: null, + firstName: "Alicja", + lastName: "Kowalska", + }); + expect(h.importAvatar).not.toHaveBeenCalled(); + expect(h.sources).toEqual([]); + }); + + it("refuses a provider account that already belongs to another member", async () => { + const h = harness(); + h.connect(BOB.id, "google", "google-alice"); + h.signInAs(ALICE); + + const response = await connectGoogle(h); + + expect(response.status).toBe(409); + expect(await errorOf(response)).toBe("account_taken"); + expect(h.connections).toEqual([ + expect.objectContaining({ userId: BOB.id }), + ]); + }); + + it("allows one account per provider", async () => { + const h = harness(); + h.connect(ALICE.id, "google", "google-work"); + h.signInAs(ALICE); + + const response = await h.request( + "/google/authorize", + json({ intent: "link" }), + ); + + expect(response.status).toBe(409); + expect(await errorOf(response)).toBe("provider_already_connected"); + }); + + it("refuses a second account that raced in after the round trip started", async () => { + const h = harness(); + h.signInAs(ALICE); + h.profiles.set("code-google", { + email: "a@gmail.test", + id: "google-alice", + username: "A", + }); + const state = await h.startRoundTrip("google", { intent: "link" }); + h.connect(ALICE.id, "google", "google-other"); + + const response = await h.finish("google", "code-google", state); + + expect(response.status).toBe(409); + expect(await errorOf(response)).toBe("provider_already_connected"); + }); + + it("answers 404 for a provider this site does not configure", async () => { + const h = harness(); + h.signInAs(ALICE); + + const response = await h.request( + "/github/authorize", + json({ intent: "link" }), + ); + + expect(response.status).toBe(404); + }); +}); + +describe("the state of a settings round trip", () => { + const prepare = async () => { + const h = harness(); + h.signInAs(ALICE); + h.profiles.set("code-google", { + email: "a@gmail.test", + id: "google-alice", + username: "A", + }); + const state = await h.startRoundTrip("google", { intent: "link" }); + + return { h, state }; + }; + + it("is stored hashed, never as the value in the URL", async () => { + const { h, state } = await prepare(); + + expect(h.operations()).toHaveLength(1); + expect(h.operations()[0]?.tokenHash).not.toBe(state); + expect(h.operations()[0]?.tokenHash).toMatch(/^[\da-f]{64}$/); + }); + + it("rejects a state the server never issued, before talking to the provider", async () => { + const { h } = await prepare(); + + const response = await h.finish( + "google", + "code-google", + `cl_${"f".repeat(64)}`, + ); + + expect(response.status).toBe(400); + expect(await errorOf(response)).toBe("invalid_state"); + expect(h.google.fetchToken).not.toHaveBeenCalled(); + }); + + it("rejects the public sign-in state", async () => { + const { h } = await prepare(); + + const response = await h.finish( + "google", + "code-google", + "0123456789abcdef", + ); + + expect(response.status).toBe(400); + expect(h.connections).toEqual([]); + }); + + it("expires after ten minutes", async () => { + const { h, state } = await prepare(); + vi.useFakeTimers({ toFake: ["Date"] }); + vi.setSystemTime(Date.now() + SSO_CONNECTION_STATE_TTL_MS + 1000); + + const response = await h.finish("google", "code-google", state); + + expect(response.status).toBe(400); + expect(h.connections).toEqual([]); + }); + + it("works once", async () => { + const { h, state } = await prepare(); + + expect((await h.finish("google", "code-google", state)).status).toBe(200); + const replay = await h.finish("google", "code-google", state); + + expect(replay.status).toBe(400); + expect(await errorOf(replay)).toBe("invalid_state"); + }); + + it("belongs to the provider it was issued for", async () => { + const { h, state } = await prepare(); + + const response = await h.finish("discord", "code-google", state); + + expect(response.status).toBe(400); + expect(h.connections).toEqual([]); + }); + + it("belongs to the intent it was issued for", async () => { + const { h, state } = await prepare(); + h.connect(ALICE.id, "discord", "discord-alice"); + const importState = await h.startRoundTrip("discord", { + fields: ["avatar"], + intent: "import", + }); + + const asLink = await h.finish( + "discord", + "code-google", + importState.replace(/^ci_/, "cl_"), + ); + + expect(asLink.status).toBe(400); + expect(state).toMatch(/^cl_/); + }); + + it("belongs to the member who started it, and is spent by a stranger's attempt", async () => { + const { h, state } = await prepare(); + h.signInAs(BOB); + + const stolen = await h.finish("google", "code-google", state); + + expect(stolen.status).toBe(400); + expect(h.connections).toEqual([]); + + h.signInAs(ALICE); + expect((await h.finish("google", "code-google", state)).status).toBe(400); + }); + + it("needs a signed-in member at both ends", async () => { + const { h, state } = await prepare(); + h.signInAs(null); + + expect((await h.finish("google", "code-google", state)).status).toBe(401); + expect( + (await h.request("/google/authorize", json({ intent: "link" }))).status, + ).toBe(401); + expect((await h.request("/")).status).toBe(401); + expect((await h.request("/google", { method: "DELETE" })).status).toBe(401); + expect( + (await h.request("/preferences", json({ sources: {}, sync: {} }, "PUT"))) + .status, + ).toBe(401); + }); +}); + +describe("disconnecting an account", () => { + it("removes the connection and its sync settings, and nothing at the provider", async () => { + const h = harness(); + h.connect(ALICE.id, "google", "google-alice"); + h.sources.push({ field: "avatar", providerId: "google", userId: ALICE.id }); + h.signInAs(ALICE); + + const response = await h.request("/google", { method: "DELETE" }); + + expect(response.status).toBe(200); + expect(h.connections).toEqual([]); + expect(h.sources).toEqual([]); + expect(h.users.get(ALICE.id)?.firstName).toBe("Alicja"); + expect(h.google.fetchToken).not.toHaveBeenCalled(); + expect(h.emit).toHaveBeenCalledWith("user.sso.unlinked", { + providerId: "google", + userId: ALICE.id, + }); + }); + + it("only ever touches the signed-in member's own connection", async () => { + const h = harness(); + h.connect(ALICE.id, "google", "google-alice"); + h.signInAs(BOB); + + const response = await h.request("/google", { method: "DELETE" }); + + expect(response.status).toBe(404); + expect(h.connections).toHaveLength(1); + }); + + it.each([ + { + expected: 200, + name: "a password, with password sign-in on", + setup: { hasPassword: true, passkeys: 0 }, + toggles: { passkeysEnabled: false, passwordEnabled: true }, + }, + { + expected: 409, + name: "a password, with password sign-in off", + setup: { hasPassword: true, passkeys: 0 }, + toggles: { passkeysEnabled: false, passwordEnabled: false }, + }, + { + expected: 200, + name: "a passkey, with passkeys on", + setup: { hasPassword: false, passkeys: 1 }, + toggles: { passkeysEnabled: true, passwordEnabled: true }, + }, + { + expected: 409, + name: "a passkey, with passkeys off", + setup: { hasPassword: false, passkeys: 1 }, + toggles: { passkeysEnabled: false, passwordEnabled: true }, + }, + { + expected: 409, + name: "nothing else at all", + setup: { hasPassword: false, passkeys: 0 }, + toggles: { passkeysEnabled: true, passwordEnabled: true }, + }, + ])("with $name, answers $expected", async ({ expected, setup, toggles }) => { + const h = harness({ + accounts: [account(1, setup)], + ...toggles, + }); + h.connect(1, "google", "google-1"); + h.signInAs(account(1, setup)); + + const response = await h.request("/google", { method: "DELETE" }); + + expect(response.status).toBe(expected); + if (expected === 409) { + expect(await errorOf(response)).toBe("last_sign_in_method"); + expect(h.connections).toHaveLength(1); + } + }); + + it("counts another connection only while its provider is still configured", async () => { + const lonely = account(1, { hasPassword: false }); + const h = harness({ accounts: [lonely] }); + h.connect(1, "google", "google-1"); + h.connect(1, "discord", "discord-1"); + h.signInAs(lonely); + h.setAdapters([h.google]); + + expect((await h.request("/google", { method: "DELETE" })).status).toBe(409); + + h.setAdapters([h.google, h.discord]); + expect((await h.request("/google", { method: "DELETE" })).status).toBe(200); + }); + + it("keeps one way in when two disconnects race each other", async () => { + const lonely = account(1, { hasPassword: false }); + const h = harness({ accounts: [lonely] }); + h.connect(1, "google", "google-1"); + h.connect(1, "discord", "discord-1"); + h.signInAs(lonely); + + const statuses = ( + await Promise.all([ + h.request("/google", { method: "DELETE" }), + h.request("/discord", { method: "DELETE" }), + ]) + ).map(response => response.status); + + expect(statuses.toSorted((a, b) => a - b)).toEqual([200, 409]); + expect(h.connections).toHaveLength(1); + }); +}); + +describe("choosing where profile fields come from", () => { + const withBoth = () => { + const h = harness(); + h.connect(ALICE.id, "google", "google-alice"); + h.connect(ALICE.id, "discord", "discord-alice"); + h.signInAs(ALICE); + + return h; + }; + + const save = async (h: Harness, body: unknown) => + await h.request("/preferences", json(body, "PUT")); + + it("keeps a single provider per field", async () => { + const h = withBoth(); + + await save(h, { sources: { avatar: "google" }, sync: {} }); + const response = await save(h, { + sources: { avatar: "discord", firstName: "google" }, + sync: { discord: true }, + }); + + expect(response.status).toBe(200); + expect( + h.sources.toSorted((a, b) => a.field.localeCompare(b.field)), + ).toEqual([ + { field: "avatar", providerId: "discord", userId: ALICE.id }, + { field: "firstName", providerId: "google", userId: ALICE.id }, + ]); + expect( + h.connections.find(one => one.providerId === "discord")?.syncOnSignIn, + ).toBe(true); + }); + + it("puts a field back to manual with null", async () => { + const h = withBoth(); + await save(h, { sources: { avatar: "google" }, sync: {} }); + + await save(h, { sources: { avatar: null }, sync: {} }); + + expect(h.sources).toEqual([]); + }); + + it("refuses a provider that cannot supply the field", async () => { + const h = withBoth(); + + const response = await save(h, { + sources: { firstName: "discord" }, + sync: {}, + }); + + expect(response.status).toBe(400); + expect(await errorOf(response)).toBe("invalid_source"); + expect(h.sources).toEqual([]); + }); + + it("refuses a provider the member has not connected", async () => { + const h = harness(); + h.signInAs(ALICE); + + const sources = await save(h, { sources: { avatar: "google" }, sync: {} }); + const sync = await save(h, { sources: {}, sync: { google: true } }); + + expect(sources.status).toBe(400); + expect(sync.status).toBe(400); + }); + + it("starts every connection manual, with sign-in updates off", async () => { + const h = withBoth(); + + const response = await h.request("/"); + const body = (await response.json()) as { + providers: { + connection: null | { syncOnSignIn: boolean }; + id: string; + profileFields: string[]; + }[]; + sources: Record; + }; + + expect(body.sources).toEqual({ + avatar: null, + firstName: null, + lastName: null, + }); + expect( + body.providers.map(one => [ + one.id, + one.connection?.syncOnSignIn, + one.profileFields, + ]), + ).toEqual([ + ["google", false, ["avatar", "firstName", "lastName"]], + ["discord", false, ["avatar"]], + ]); + }); +}); + +describe("importing a profile now", () => { + const importFrom = async ( + h: Harness, + fields: string[], + profile: Partial = {}, + ) => { + h.profiles.set("code-import", { + avatarUrl: "https://lh3.googleusercontent.test/a/new.png", + email: "alice.personal@gmail.test", + firstName: "Alice", + id: "google-alice", + lastName: "Smith", + username: "Alice Smith", + ...profile, + }); + const state = await h.startRoundTrip("google", { + fields, + intent: "import", + }); + + return await h.finish("google", "code-import", state); + }; + + const prepare = (overrides: Partial = {}) => { + const alice = { ...ALICE, ...overrides }; + const h = harness({ accounts: [alice, BOB] }); + h.connect(ALICE.id, "google", "google-alice"); + h.signInAs(alice); + + return h; + }; + + it("shows a preview first and changes nothing until the member confirms", async () => { + const h = prepare(); + + const callback = await importFrom(h, ["firstName", "lastName"]); + expect(callback.status).toBe(200); + expect(await callback.json()).toEqual({ + intent: "import", + providerId: "google", + }); + + const preview = await h.request("/google/import"); + expect(preview.status).toBe(200); + expect(((await preview.json()) as { fields: unknown[] }).fields).toEqual([ + expect.objectContaining({ + current: "Alicja", + field: "firstName", + incoming: "Alice", + }), + expect.objectContaining({ + current: "Kowalska", + field: "lastName", + incoming: "Smith", + }), + ]); + expect(h.users.get(ALICE.id)?.firstName).toBe("Alicja"); + }); + + it("applies only the confirmed fields, once, and leaves showRealName alone", async () => { + const h = prepare({ showRealName: false }); + await importFrom(h, ["firstName", "lastName"]); + + const response = await h.request( + "/google/import", + json({ fields: ["firstName"] }), + ); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ + manualFields: [], + results: { firstName: "updated" }, + }); + expect(h.users.get(ALICE.id)).toMatchObject({ + firstName: "Alice", + lastName: "Kowalska", + showRealName: false, + }); + expect(h.emit).toHaveBeenCalledWith("user.sso.profile_synced", { + fields: ["firstName"], + providerId: "google", + trigger: "import", + userId: ALICE.id, + }); + + const replay = await h.request( + "/google/import", + json({ fields: ["firstName"] }), + ); + expect(replay.status).toBe(404); + }); + + it("keeps local values the provider did not send", async () => { + const h = prepare(); + await importFrom(h, ["firstName", "lastName"], { + firstName: " ", + lastName: undefined, + }); + + const response = await h.request( + "/google/import", + json({ fields: ["firstName", "lastName"] }), + ); + + expect(await response.json()).toEqual({ + manualFields: [], + results: { firstName: "missing", lastName: "missing" }, + }); + expect(h.users.get(ALICE.id)).toMatchObject({ + firstName: "Alicja", + lastName: "Kowalska", + }); + }); + + it("respects a role that may not edit personal information", async () => { + const h = prepare(); + h.setPolicy({ ...OPEN_POLICY, firstName: false, lastName: false }); + await importFrom(h, ["firstName"]); + + const response = await h.request( + "/google/import", + json({ fields: ["firstName"] }), + ); + + expect(await response.json()).toMatchObject({ + results: { firstName: "not_allowed" }, + }); + expect(h.users.get(ALICE.id)?.firstName).toBe("Alicja"); + }); + + it("refuses fields that were not part of the round trip", async () => { + const h = prepare(); + await importFrom(h, ["firstName"]); + + const response = await h.request( + "/google/import", + json({ fields: ["lastName"] }), + ); + + expect(response.status).toBe(400); + expect(h.users.get(ALICE.id)?.lastName).toBe("Kowalska"); + }); + + it("refuses fields the provider cannot supply at all", async () => { + const h = harness(); + h.connect(ALICE.id, "discord", "discord-alice"); + h.signInAs(ALICE); + + const response = await h.request( + "/discord/authorize", + json({ fields: ["firstName"], intent: "import" }), + ); + + expect(response.status).toBe(400); + expect(await errorOf(response)).toBe("invalid_fields"); + }); + + it("needs the provider to be connected first", async () => { + const h = harness(); + h.signInAs(ALICE); + + const response = await h.request( + "/google/authorize", + json({ fields: ["avatar"], intent: "import" }), + ); + + expect(response.status).toBe(404); + }); + + it("refuses a different account at the provider", async () => { + const h = prepare(); + + const response = await importFrom(h, ["firstName"], { id: "google-bob" }); + + expect(response.status).toBe(409); + expect(await errorOf(response)).toBe("account_mismatch"); + expect((await h.request("/google/import")).status).toBe(404); + }); + + it("keeps the current avatar when the download fails", async () => { + const h = prepare({ avatarId: 42 }); + h.importAvatar.mockRejectedValueOnce(new Error("blocked_address")); + await importFrom(h, ["avatar"]); + + const response = await h.request( + "/google/import", + json({ fields: ["avatar"] }), + ); + + expect(response.status).toBe(200); + expect(await response.json()).toMatchObject({ + results: { avatar: "failed" }, + }); + expect(h.users.get(ALICE.id)?.avatarId).toBe(42); + }); + + it("switches a field synced from another provider to manual", async () => { + const h = prepare(); + h.connect(ALICE.id, "discord", "discord-alice"); + h.sources.push({ + field: "avatar", + providerId: "discord", + userId: ALICE.id, + }); + await importFrom(h, ["avatar"]); + + const response = await h.request( + "/google/import", + json({ fields: ["avatar"] }), + ); + + expect(await response.json()).toEqual({ + manualFields: ["avatar"], + results: { avatar: "updated" }, + }); + expect(h.sources).toEqual([]); + }); +}); + +describe("updating the profile on sign-in", () => { + const signIn = async (h: Harness, profile: Partial) => { + const adapter = h.google; + const user: SSOProviderUser = { + email: "Alice.Personal@gmail.test", + id: "google-alice", + username: "Alice Smith", + ...profile, + }; + + await new SsoConnectionModel(h.context()).afterSignIn({ + identity: ssoIdentityOf(adapter, user), + providerId: "google", + userId: ALICE.id, + }); + }; + + const prepare = ({ sync }: { sync: boolean }) => { + const h = harness(); + h.connect(ALICE.id, "google", "google-alice", { syncOnSignIn: sync }); + h.sources.push( + { field: "firstName", providerId: "google", userId: ALICE.id }, + { field: "avatar", providerId: "google", userId: ALICE.id }, + ); + + return h; + }; + + it("changes nothing while sign-in updates are off", async () => { + const h = prepare({ sync: false }); + + await signIn(h, { firstName: "Alice", lastName: "Smith" }); + + expect(h.users.get(ALICE.id)?.firstName).toBe("Alicja"); + expect(h.importAvatar).not.toHaveBeenCalled(); + }); + + it("updates only the fields sourced from this provider", async () => { + const h = prepare({ sync: true }); + + await signIn(h, { + avatarUrl: "https://lh3.googleusercontent.test/a/new.png", + firstName: "Alice", + lastName: "Smith", + }); + + expect(h.users.get(ALICE.id)).toMatchObject({ + avatarId: 500, + firstName: "Alice", + lastName: "Kowalska", + showRealName: false, + }); + expect(h.emit).toHaveBeenCalledWith("user.sso.profile_synced", { + fields: ["avatar", "firstName"], + providerId: "google", + trigger: "sign_in", + userId: ALICE.id, + }); + }); + + it("stops updating a field the member edited by hand", async () => { + const h = prepare({ sync: true }); + + await new SsoConnectionModel(h.context()).clearSourcesAfterManualEdit({ + fields: ["firstName"], + userId: ALICE.id, + }); + await signIn(h, { firstName: "Alice" }); + + expect(h.users.get(ALICE.id)?.firstName).toBe("Alicja"); + expect(h.sources).toEqual([ + { field: "avatar", providerId: "google", userId: ALICE.id }, + ]); + }); + + it("never erases a value the provider left out", async () => { + const h = prepare({ sync: true }); + + await signIn(h, { firstName: "" }); + + expect(h.users.get(ALICE.id)?.firstName).toBe("Alicja"); + expect(h.importAvatar).not.toHaveBeenCalled(); + }); + + it("finishes the sign-in even when the avatar cannot be fetched", async () => { + const h = prepare({ sync: true }); + h.importAvatar.mockRejectedValueOnce(new Error("timeout")); + + await expect( + signIn(h, { + avatarUrl: "https://lh3.googleusercontent.test/a/new.png", + firstName: "Alice", + }), + ).resolves.toBeUndefined(); + + expect(h.users.get(ALICE.id)).toMatchObject({ + avatarId: null, + firstName: "Alice", + }); + }); + + it("refreshes the provider email as connection info only", async () => { + const h = prepare({ sync: false }); + + await signIn(h, {}); + + expect(h.connections[0]?.providerEmail).toBe("alice.personal@gmail.test"); + expect(h.users.get(ALICE.id)?.email).toBe(ALICE.email); + }); +}); + +describe("syncing now, without signing out", () => { + const prepare = () => { + const h = harness(); + h.connect(ALICE.id, "google", "google-alice"); + h.sources.push( + { field: "firstName", providerId: "google", userId: ALICE.id }, + { field: "avatar", providerId: "google", userId: ALICE.id }, + ); + h.signInAs(ALICE); + + return h; + }; + + const syncFrom = async (h: Harness, profile: Partial) => { + h.profiles.set("code-sync", { + email: "alice.personal@gmail.test", + id: "google-alice", + username: "Alice Smith", + ...profile, + }); + const state = await h.startRoundTrip("google", { intent: "sync" }); + + return await h.finish("google", "code-sync", state); + }; + + it("re-authenticates and updates the fields sourced from this provider", async () => { + const h = prepare(); + + const response = await syncFrom(h, { + avatarUrl: "https://lh3.googleusercontent.test/a/new.png", + firstName: "Alice", + lastName: "Smith", + }); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ + intent: "sync", + providerId: "google", + results: { avatar: "updated", firstName: "updated" }, + }); + expect(h.users.get(ALICE.id)).toMatchObject({ + avatarId: 500, + firstName: "Alice", + lastName: "Kowalska", + showRealName: false, + }); + expect(h.emit).toHaveBeenCalledWith("user.sso.profile_synced", { + fields: ["avatar", "firstName"], + providerId: "google", + trigger: "sync", + userId: ALICE.id, + }); + }); + + it("works while sign-in updates are off, and never touches the session", async () => { + const h = prepare(); + + await syncFrom(h, { firstName: "Alice" }); + + expect(h.connections[0]?.syncOnSignIn).toBe(false); + expect(h.users.get(ALICE.id)?.firstName).toBe("Alice"); + expect(h.createSession).not.toHaveBeenCalled(); + expect(h.createAdminSession).not.toHaveBeenCalled(); + }); + + it("has nothing to do until a field is sourced from the provider", async () => { + const h = harness(); + h.connect(ALICE.id, "google", "google-alice"); + h.signInAs(ALICE); + + const response = await h.request( + "/google/authorize", + json({ intent: "sync" }), + ); + + expect(response.status).toBe(400); + expect(await errorOf(response)).toBe("nothing_to_sync"); + }); + + it("ignores field choices sent by the browser", async () => { + const h = prepare(); + h.profiles.set("code-sync", { + email: "a@gmail.test", + firstName: "Alice", + id: "google-alice", + lastName: "Smith", + username: "A", + }); + const state = await h.startRoundTrip("google", { + fields: ["lastName"], + intent: "sync", + }); + + await h.finish("google", "code-sync", state); + + expect(h.users.get(ALICE.id)).toMatchObject({ + firstName: "Alice", + lastName: "Kowalska", + }); + }); + + it("leaves a field alone if it was switched to manual meanwhile", async () => { + const h = prepare(); + h.profiles.set("code-sync", { + email: "a@gmail.test", + firstName: "Alice", + id: "google-alice", + username: "A", + }); + const state = await h.startRoundTrip("google", { intent: "sync" }); + await new SsoConnectionModel(h.context()).clearSourcesAfterManualEdit({ + fields: ["firstName"], + userId: ALICE.id, + }); + + const response = await h.finish("google", "code-sync", state); + + expect(await response.json()).toMatchObject({ + results: { avatar: "missing" }, + }); + expect(h.users.get(ALICE.id)?.firstName).toBe("Alicja"); + }); + + it("keeps local values the provider did not send", async () => { + const h = prepare(); + + const response = await syncFrom(h, { firstName: " " }); + + expect(await response.json()).toMatchObject({ + results: { avatar: "missing", firstName: "missing" }, + }); + expect(h.users.get(ALICE.id)?.firstName).toBe("Alicja"); + }); + + it("refuses a different account at the provider", async () => { + const h = prepare(); + + const response = await syncFrom(h, { firstName: "Bob", id: "google-bob" }); + + expect(response.status).toBe(409); + expect(await errorOf(response)).toBe("account_mismatch"); + expect(h.users.get(ALICE.id)?.firstName).toBe("Alicja"); + }); +}); diff --git a/packages/vitnode/src/api/modules/users/sso/connections/failure.ts b/packages/vitnode/src/api/modules/users/sso/connections/failure.ts new file mode 100644 index 000000000..b170ce5af --- /dev/null +++ b/packages/vitnode/src/api/modules/users/sso/connections/failure.ts @@ -0,0 +1,20 @@ +import type { Context } from "hono"; + +import { SsoConnectionError } from "@/api/models/sso-connection"; + +export const ssoConnectionFailure = (c: Context, error: unknown) => { + if (!(error instanceof SsoConnectionError)) throw error; + + const body = { error: error.code }; + + switch (error.status) { + case 400: + return c.json(body, 400); + case 404: + return c.json(body, 404); + case 409: + return c.json(body, 409); + case 502: + return c.json(body, 502); + } +}; diff --git a/packages/vitnode/src/api/modules/users/sso/connections/routes/authorize.route.ts b/packages/vitnode/src/api/modules/users/sso/connections/routes/authorize.route.ts new file mode 100644 index 000000000..2b7a90e22 --- /dev/null +++ b/packages/vitnode/src/api/modules/users/sso/connections/routes/authorize.route.ts @@ -0,0 +1,63 @@ +import { z } from "@hono/zod-openapi"; + +import { buildRoute } from "@/api/lib/route"; +import { SsoConnectionModel } from "@/api/models/sso-connection"; +import { CONFIG_PLUGIN } from "@/config"; +import { SSO_CONNECTION_INTENTS, SSO_PROFILE_FIELDS } from "@/lib/sso-profile"; + +import { requireSignedInUser } from "../../../passkeys/failure"; +import { ssoConnectionFailure } from "../failure"; +import { + SSO_CONNECTION_ERROR_RESPONSES, + zodSsoProfileField, + zodSsoProviderIdParam, +} from "../schema"; + +export const zodSsoAuthorizeSchema = z.object({ + fields: z.array(zodSsoProfileField).max(SSO_PROFILE_FIELDS.length).optional(), + intent: z.enum(SSO_CONNECTION_INTENTS), +}); + +export const authorizeSsoConnectionRoute = buildRoute({ + pluginId: CONFIG_PLUGIN.pluginId, + route: { + method: "post", + description: + "Start a provider round trip for the signed-in user - to connect a new account, to import chosen profile fields, or to sync the fields sourced from a connected account right away (no sign-out needed).", + path: "/{providerId}/authorize", + request: { + params: z.object({ providerId: zodSsoProviderIdParam }), + body: { + required: true, + content: { "application/json": { schema: zodSsoAuthorizeSchema } }, + }, + }, + responses: { + 200: { + content: { + "application/json": { schema: z.object({ url: z.string() }) }, + }, + description: "The provider URL to send the browser to", + }, + ...SSO_CONNECTION_ERROR_RESPONSES, + }, + }, + handler: async c => { + const user = requireSignedInUser(c.get("user")); + const { providerId } = c.req.valid("param"); + const { fields, intent } = c.req.valid("json"); + + try { + const { url } = await new SsoConnectionModel(c).authorize({ + fields, + intent, + providerId, + userId: user.id, + }); + + return c.json({ url }, 200); + } catch (error) { + return ssoConnectionFailure(c, error); + } + }, +}); diff --git a/packages/vitnode/src/api/modules/users/sso/connections/routes/callback.route.ts b/packages/vitnode/src/api/modules/users/sso/connections/routes/callback.route.ts new file mode 100644 index 000000000..366b8b65c --- /dev/null +++ b/packages/vitnode/src/api/modules/users/sso/connections/routes/callback.route.ts @@ -0,0 +1,73 @@ +import { z } from "@hono/zod-openapi"; + +import { buildRoute } from "@/api/lib/route"; +import { SsoConnectionModel } from "@/api/models/sso-connection"; +import { CONFIG_PLUGIN } from "@/config"; +import { SSO_CONNECTION_INTENTS } from "@/lib/sso-profile"; + +import { requireSignedInUser } from "../../../passkeys/failure"; +import { ssoConnectionFailure } from "../failure"; +import { + SSO_CONNECTION_ERROR_RESPONSES, + zodSsoFieldOutcomes, + zodSsoProviderIdParam, +} from "../schema"; + +export const zodSsoConnectionCallbackSchema = z.object({ + code: z.string().min(1).max(4096), + state: z.string().min(16).max(256), +}); + +export const ssoConnectionCallbackRoute = buildRoute({ + pluginId: CONFIG_PLUGIN.pluginId, + route: { + method: "post", + description: + "Finish a provider round trip started from settings - connect, import or sync. The state must belong to the signed-in user, this provider and this intent, and works once.", + path: "/{providerId}/callback", + request: { + params: z.object({ providerId: zodSsoProviderIdParam }), + body: { + required: true, + content: { + "application/json": { schema: zodSsoConnectionCallbackSchema }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: z.object({ + intent: z.enum(SSO_CONNECTION_INTENTS), + providerId: z.string(), + results: zodSsoFieldOutcomes.optional(), + }), + }, + }, + description: + "The account was connected, or its profile is ready to review", + }, + ...SSO_CONNECTION_ERROR_RESPONSES, + }, + }, + handler: async c => { + const user = requireSignedInUser(c.get("user")); + const { providerId } = c.req.valid("param"); + const { code, state } = c.req.valid("json"); + + try { + return c.json( + await new SsoConnectionModel(c).callback({ + code, + providerId, + state, + user, + }), + 200, + ); + } catch (error) { + return ssoConnectionFailure(c, error); + } + }, +}); diff --git a/packages/vitnode/src/api/modules/users/sso/connections/routes/disconnect.route.ts b/packages/vitnode/src/api/modules/users/sso/connections/routes/disconnect.route.ts new file mode 100644 index 000000000..0083045c1 --- /dev/null +++ b/packages/vitnode/src/api/modules/users/sso/connections/routes/disconnect.route.ts @@ -0,0 +1,44 @@ +import { z } from "@hono/zod-openapi"; + +import { buildRoute } from "@/api/lib/route"; +import { SsoConnectionModel } from "@/api/models/sso-connection"; +import { CONFIG_PLUGIN } from "@/config"; + +import { requireSignedInUser } from "../../../passkeys/failure"; +import { ssoConnectionFailure } from "../failure"; +import { + SSO_CONNECTION_ERROR_RESPONSES, + zodSsoProviderIdParam, +} from "../schema"; + +export const disconnectSsoConnectionRoute = buildRoute({ + pluginId: CONFIG_PLUGIN.pluginId, + route: { + method: "delete", + description: + "Remove the signed-in user's connection to a provider and its profile sync settings. Refused when it is the account's last way to sign in. Does not revoke anything at the provider.", + path: "/{providerId}", + request: { + params: z.object({ providerId: zodSsoProviderIdParam }), + }, + responses: { + 200: { description: "Connection removed" }, + ...SSO_CONNECTION_ERROR_RESPONSES, + }, + }, + handler: async c => { + const user = requireSignedInUser(c.get("user")); + const { providerId } = c.req.valid("param"); + + try { + await new SsoConnectionModel(c).disconnect({ + providerId, + userId: user.id, + }); + + return c.body(null, 200); + } catch (error) { + return ssoConnectionFailure(c, error); + } + }, +}); diff --git a/packages/vitnode/src/api/modules/users/sso/connections/routes/import.route.ts b/packages/vitnode/src/api/modules/users/sso/connections/routes/import.route.ts new file mode 100644 index 000000000..700140c6a --- /dev/null +++ b/packages/vitnode/src/api/modules/users/sso/connections/routes/import.route.ts @@ -0,0 +1,123 @@ +import { z } from "@hono/zod-openapi"; + +import { buildRoute } from "@/api/lib/route"; +import { SsoConnectionModel } from "@/api/models/sso-connection"; +import { CONFIG_PLUGIN } from "@/config"; +import { SSO_PROFILE_FIELDS } from "@/lib/sso-profile"; + +import { requireSignedInUser } from "../../../passkeys/failure"; +import { ssoConnectionFailure } from "../failure"; +import { + SSO_CONNECTION_ERROR_RESPONSES, + zodSsoImportPreview, + zodSsoImportResult, + zodSsoProfileField, + zodSsoProviderIdParam, +} from "../schema"; + +const params = z.object({ providerId: zodSsoProviderIdParam }); + +export const ssoImportPreviewRoute = buildRoute({ + pluginId: CONFIG_PLUGIN.pluginId, + route: { + method: "get", + description: + "The profile values fetched by the signed-in user's last import round trip, next to the current ones.", + path: "/{providerId}/import", + request: { params }, + responses: { + 200: { + content: { "application/json": { schema: zodSsoImportPreview } }, + description: "The import preview", + }, + ...SSO_CONNECTION_ERROR_RESPONSES, + }, + }, + handler: async c => { + const user = requireSignedInUser(c.get("user")); + const { providerId } = c.req.valid("param"); + + try { + return c.json( + await new SsoConnectionModel(c).preview({ + providerId, + userId: user.id, + }), + 200, + ); + } catch (error) { + return ssoConnectionFailure(c, error); + } + }, +}); + +export const zodSsoApplyImportSchema = z.object({ + fields: z.array(zodSsoProfileField).min(1).max(SSO_PROFILE_FIELDS.length), +}); + +export const ssoApplyImportRoute = buildRoute({ + pluginId: CONFIG_PLUGIN.pluginId, + route: { + method: "post", + description: + "Copy the selected fields of the import preview onto the signed-in user's profile. The preview works once.", + path: "/{providerId}/import", + request: { + params, + body: { + required: true, + content: { "application/json": { schema: zodSsoApplyImportSchema } }, + }, + }, + responses: { + 200: { + content: { "application/json": { schema: zodSsoImportResult } }, + description: "What happened to each selected field", + }, + ...SSO_CONNECTION_ERROR_RESPONSES, + }, + }, + handler: async c => { + const user = requireSignedInUser(c.get("user")); + const { providerId } = c.req.valid("param"); + const { fields } = c.req.valid("json"); + + try { + return c.json( + await new SsoConnectionModel(c).applyImport({ + fields, + providerId, + userId: user.id, + }), + 200, + ); + } catch (error) { + return ssoConnectionFailure(c, error); + } + }, +}); + +export const ssoDiscardImportRoute = buildRoute({ + pluginId: CONFIG_PLUGIN.pluginId, + route: { + method: "delete", + description: "Throw away the signed-in user's pending import preview.", + path: "/{providerId}/import", + request: { params }, + responses: { + 200: { description: "Preview discarded" }, + ...SSO_CONNECTION_ERROR_RESPONSES, + }, + }, + handler: async c => { + const user = requireSignedInUser(c.get("user")); + const { providerId } = c.req.valid("param"); + + await new SsoConnectionModel(c).discardPreview({ + providerId, + userId: user.id, + }); + + return c.body(null, 200); + }, +}); diff --git a/packages/vitnode/src/api/modules/users/sso/connections/routes/list.route.ts b/packages/vitnode/src/api/modules/users/sso/connections/routes/list.route.ts new file mode 100644 index 000000000..86f19f806 --- /dev/null +++ b/packages/vitnode/src/api/modules/users/sso/connections/routes/list.route.ts @@ -0,0 +1,38 @@ +import { buildRoute } from "@/api/lib/route"; +import { SsoConnectionModel } from "@/api/models/sso-connection"; +import { CONFIG_PLUGIN } from "@/config"; + +import { requireSignedInUser } from "../../../passkeys/failure"; +import { ssoConnectionFailure } from "../failure"; +import { + SSO_CONNECTION_ERROR_RESPONSES, + zodSsoConnectionsOverview, +} from "../schema"; + +export const listSsoConnectionsRoute = buildRoute({ + pluginId: CONFIG_PLUGIN.pluginId, + route: { + method: "get", + description: + "List the configured SSO providers with the signed-in user's connection to each, and where their profile fields come from.", + path: "/", + responses: { + 200: { + content: { + "application/json": { schema: zodSsoConnectionsOverview }, + }, + description: "Providers, connections and profile sources", + }, + ...SSO_CONNECTION_ERROR_RESPONSES, + }, + }, + handler: async c => { + const user = requireSignedInUser(c.get("user")); + + try { + return c.json(await new SsoConnectionModel(c).overview(user.id), 200); + } catch (error) { + return ssoConnectionFailure(c, error); + } + }, +}); diff --git a/packages/vitnode/src/api/modules/users/sso/connections/routes/preferences.route.ts b/packages/vitnode/src/api/modules/users/sso/connections/routes/preferences.route.ts new file mode 100644 index 000000000..1f1bbe38d --- /dev/null +++ b/packages/vitnode/src/api/modules/users/sso/connections/routes/preferences.route.ts @@ -0,0 +1,61 @@ +import { z } from "@hono/zod-openapi"; + +import { buildRoute } from "@/api/lib/route"; +import { SsoConnectionModel } from "@/api/models/sso-connection"; +import { CONFIG_PLUGIN } from "@/config"; + +import { requireSignedInUser } from "../../../passkeys/failure"; +import { ssoConnectionFailure } from "../failure"; +import { + SSO_CONNECTION_ERROR_RESPONSES, + zodSsoProviderIdParam, +} from "../schema"; + +const zodSource = zodSsoProviderIdParam.nullable().optional(); + +export const zodSsoPreferencesSchema = z.object({ + sources: z.object({ + avatar: zodSource, + firstName: zodSource, + lastName: zodSource, + }), + sync: z + .record(zodSsoProviderIdParam, z.boolean()) + .refine(value => Object.keys(value).length <= 32), +}); + +export const ssoPreferencesRoute = buildRoute({ + pluginId: CONFIG_PLUGIN.pluginId, + route: { + method: "put", + description: + "Choose which connected provider each profile field comes from (null keeps it managed manually), and whether signing in through a provider updates those fields.", + path: "/preferences", + request: { + body: { + required: true, + content: { "application/json": { schema: zodSsoPreferencesSchema } }, + }, + }, + responses: { + 200: { description: "Preferences saved" }, + ...SSO_CONNECTION_ERROR_RESPONSES, + }, + }, + handler: async c => { + const user = requireSignedInUser(c.get("user")); + const { sources, sync } = c.req.valid("json"); + + try { + await new SsoConnectionModel(c).savePreferences({ + sources, + sync, + userId: user.id, + }); + + return c.body(null, 200); + } catch (error) { + return ssoConnectionFailure(c, error); + } + }, +}); diff --git a/packages/vitnode/src/api/modules/users/sso/connections/schema.ts b/packages/vitnode/src/api/modules/users/sso/connections/schema.ts new file mode 100644 index 000000000..210e4dfbf --- /dev/null +++ b/packages/vitnode/src/api/modules/users/sso/connections/schema.ts @@ -0,0 +1,113 @@ +import { z } from "@hono/zod-openapi"; + +import type { SsoConnectionErrorCode } from "@/api/models/sso-connection"; + +import { SSO_PROFILE_FIELDS } from "@/lib/sso-profile"; + +const ERROR_CODES = [ + "account_mismatch", + "account_taken", + "invalid_fields", + "invalid_source", + "invalid_state", + "last_sign_in_method", + "not_connected", + "nothing_to_sync", + "preview_not_found", + "provider_already_connected", + "provider_error", + "provider_not_found", +] as const satisfies readonly SsoConnectionErrorCode[]; + +export const zodSsoConnectionErrorSchema = z.object({ + error: z.enum(ERROR_CODES), +}); + +const errorResponse = (description: string) => ({ + content: { + "application/json": { + schema: zodSsoConnectionErrorSchema, + }, + }, + description, +}); + +export const SSO_CONNECTION_ERROR_RESPONSES = { + 400: errorResponse("The request or its state was rejected"), + 401: { description: "Not signed in" }, + 404: errorResponse("The provider, connection or import is unknown"), + 409: errorResponse("The request conflicts with the account's connections"), + 502: errorResponse("The provider did not answer as expected"), +}; + +export const zodSsoProviderIdParam = z + .string() + .min(1) + .max(255) + .regex(/^[\w-]+$/) + .openapi({ example: "google" }); + +export const zodSsoProfileField = z.enum(SSO_PROFILE_FIELDS); + +export const zodSsoConnectionsOverview = z.object({ + providers: z.array( + z.object({ + available: z.boolean(), + connection: z + .object({ + accountLabel: z.string().nullable(), + connectedAt: z.date(), + email: z.string().nullable(), + syncOnSignIn: z.boolean(), + }) + .nullable(), + icon: z.string().nullable(), + id: z.string(), + name: z.string(), + profileFields: z.array(zodSsoProfileField), + }), + ), + signIn: z.object({ + hasPassword: z.boolean(), + passkeys: z.number(), + passkeysEnabled: z.boolean(), + passwordEnabled: z.boolean(), + }), + sources: z.object({ + avatar: z.string().nullable(), + firstName: z.string().nullable(), + lastName: z.string().nullable(), + }), +}); + +export const zodSsoImportPreview = z.object({ + expiresAt: z.date(), + fields: z.array( + z.object({ + allowed: z.boolean(), + current: z.string().nullable(), + field: zodSsoProfileField, + incoming: z.string().nullable(), + source: z.string().nullable(), + }), + ), +}); + +const zodFieldOutcome = z.enum([ + "failed", + "missing", + "not_allowed", + "unchanged", + "updated", +]); + +export const zodSsoFieldOutcomes = z.object({ + avatar: zodFieldOutcome.optional(), + firstName: zodFieldOutcome.optional(), + lastName: zodFieldOutcome.optional(), +}); + +export const zodSsoImportResult = z.object({ + manualFields: z.array(zodSsoProfileField), + results: zodSsoFieldOutcomes, +}); diff --git a/packages/vitnode/src/api/modules/users/sso/routes/callback.test.ts b/packages/vitnode/src/api/modules/users/sso/routes/callback.test.ts new file mode 100644 index 000000000..7fabfdc63 --- /dev/null +++ b/packages/vitnode/src/api/modules/users/sso/routes/callback.test.ts @@ -0,0 +1,187 @@ +// @vitest-environment node +import type { Context } from "hono"; + +import { OpenAPIHono } from "@hono/zod-openapi"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import type { EnvVariablesVitNode } from "@/api/middlewares/global.middleware"; +import type { SSOApiPlugin, SSOProviderUser } from "@/api/models/sso"; + +import { SessionModel } from "@/api/models/session"; +import { SessionAdminModel } from "@/api/models/session-admin"; +import { SsoConnectionModel } from "@/api/models/sso-connection"; + +import { ssoUserModule } from "../sso.module"; + +const PROVIDER_USER: SSOProviderUser = { + avatarUrl: "https://lh3.googleusercontent.test/a/photo.png", + email: "Alice@Example.test", + firstName: "Alice", + id: "google-alice", + lastName: "Smith", + username: "Alice Smith", +}; + +const fakeDb = (rows: unknown[][]) => { + const queue = [...rows]; + const chain = { + from: () => chain, + innerJoin: () => chain, + limit: () => chain, + then: async (onFulfilled: (value: unknown) => unknown) => + await Promise.resolve(onFulfilled(queue.shift() ?? [])), + where: () => chain, + }; + const updates = vi.fn(); + const db = { + select: () => chain, + transaction: async (run: (tx: unknown) => Promise) => + await run(db), + update: () => ({ + set: (values: unknown) => ({ + where: async () => { + updates(values); + await Promise.resolve(); + }, + }), + }), + }; + + return { db, updates }; +}; + +const harness = (rows: unknown[][]) => { + const google: SSOApiPlugin = { + fetchToken: vi.fn(async () => + Promise.resolve({ access_token: "token", token_type: "Bearer" }), + ), + fetchUser: vi.fn(async () => Promise.resolve(PROVIDER_USER)), + getUrl: ({ state }) => `https://google.example/oauth?state=${state}`, + id: "google", + name: "Google", + profileFields: ["avatar", "firstName", "lastName"], + }; + const { db, updates } = fakeDb(rows); + const createSession = vi + .spyOn(SessionModel.prototype, "createSessionByUserId") + .mockResolvedValue({ token: "session-token" }); + const createAdminSession = vi + .spyOn(SessionAdminModel.prototype, "createSessionByUserId") + .mockResolvedValue({ token: "admin-token" }); + const afterSignIn = vi + .spyOn(SsoConnectionModel.prototype, "afterSignIn") + .mockResolvedValue(undefined); + + const app = new OpenAPIHono(); + app.use("*", async (c, next) => { + c.set("core", { + authorization: { + adminCookieExpires: 1000 * 60 * 60 * 24, + adminCookieName: "vitnode_auth_admin", + cookieDomain: undefined, + cookie_expires: 1000 * 60 * 60 * 24 * 90, + cookieName: "vitnode_auth", + cookieSecure: true, + deviceCookieExpires: 1000 * 60 * 60 * 24 * 365, + deviceCookieName: "vitnode_device", + passkeys: { enabled: false, problems: [] }, + password: { enabled: true }, + ssoAdapters: [google], + }, + } as unknown as EnvVariablesVitNode["core"]); + c.set("user", null as Context["var"]["user"]); + c.set("db", db as unknown as Context["var"]["db"]); + await next(); + }); + app.route("/", ssoUserModule.hono); + + const signInThroughGoogle = async () => { + const start = await app.request("/google", { method: "POST" }); + const { url } = (await start.json()) as { url: string }; + const state = new URL(url).searchParams.get("state") ?? ""; + const cookie = start.headers + .getSetCookie() + .map(line => line.split(";")[0]) + .join("; "); + + return await app.request( + `/google/callback?code=provider-code&state=${state}`, + { headers: { cookie } }, + ); + }; + + return { + afterSignIn, + app, + createAdminSession, + createSession, + signInThroughGoogle, + updates, + }; +}; + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe("signing in through an SSO provider", () => { + it("still signs a returning member in with a public session only", async () => { + const h = harness([ + [{ email: "alice@example.test", emailVerified: true, userId: 7 }], + ]); + + const response = await h.signInThroughGoogle(); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ id: 7, token: "session-token" }); + expect(h.createSession).toHaveBeenCalledWith(7); + expect(h.createAdminSession).not.toHaveBeenCalled(); + expect(response.headers.getSetCookie().join(";")).not.toContain( + "vitnode_auth_admin", + ); + }); + + it("hands the normalized provider profile to the sign-in sync", async () => { + const h = harness([ + [{ email: "alice@example.test", emailVerified: true, userId: 7 }], + ]); + + await h.signInThroughGoogle(); + + expect(h.afterSignIn).toHaveBeenCalledWith({ + identity: { + email: "alice@example.test", + id: "google-alice", + profile: { + avatarUrl: "https://lh3.googleusercontent.test/a/photo.png", + firstName: "Alice", + lastName: "Smith", + }, + username: "Alice Smith", + }, + providerId: "google", + userId: 7, + }); + }); + + it("still confirms an unverified account whose email the provider vouches for", async () => { + const h = harness([ + [{ email: "alice@example.test", emailVerified: false, userId: 7 }], + ]); + + await h.signInThroughGoogle(); + + expect(h.updates).toHaveBeenCalledWith({ emailVerified: true }); + }); + + it("rejects a callback without the browser's state cookie", async () => { + const h = harness([]); + + const response = await h.app.request( + "/google/callback?code=provider-code&state=0123456789abcdef", + ); + + expect(response.status).toBe(400); + expect(h.createSession).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/vitnode/src/api/modules/users/sso/sso.module.ts b/packages/vitnode/src/api/modules/users/sso/sso.module.ts index face9331b..f252fffef 100644 --- a/packages/vitnode/src/api/modules/users/sso/sso.module.ts +++ b/packages/vitnode/src/api/modules/users/sso/sso.module.ts @@ -1,6 +1,7 @@ import { buildModule } from "@/api/lib/module"; import { CONFIG_PLUGIN } from "@/config"; +import { ssoConnectionsModule } from "./connections/connections.module"; import { callbackRoute } from "./routes/callback.route"; import { createUrlRoute } from "./routes/create-url.route"; import { linkRoute } from "./routes/link.route"; @@ -9,4 +10,5 @@ export const ssoUserModule = buildModule({ pluginId: CONFIG_PLUGIN.pluginId, name: "sso", routes: [callbackRoute, createUrlRoute, linkRoute], + modules: [ssoConnectionsModule], }); diff --git a/packages/vitnode/src/components/ui/alert-dialog.tsx b/packages/vitnode/src/components/ui/alert-dialog.tsx index 49f14fac4..21cc74d3d 100644 --- a/packages/vitnode/src/components/ui/alert-dialog.tsx +++ b/packages/vitnode/src/components/ui/alert-dialog.tsx @@ -71,7 +71,7 @@ function AlertDialogOverlay({ return (