diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a5c1e472a..3eb548711 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -15,7 +15,7 @@ The repository is a monorepo for the VitNode framework, which includes a backend - **Frontend:** - TanStack Start on Vite, file-based routes under `apps/web/src/routes/` - Navigation: use `@vitnode/core/tanstack/layout`'s `RouterLink`, or TanStack - Router's own `Link` / `useNavigate`. Next.js is not a dependency. + Router's own `Link` / `useNavigate`. - Forms: Use `react-hook-form@7`, `createServerFn` for mutations - UI: Shadcn UI, Tailwind CSS 4, dark/light mode with system detection - i18n: Use `use-intl`, `t('key')` for translations, `createTranslator` diff --git a/apps/api/.env.example b/apps/api/.env.example index 9726d3cbb..df075524d 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -4,10 +4,10 @@ REDIS_URL=redis://localhost:6379 # Origin of the web application (`apps/web`, `vite dev --port 3000`). This API # trusts it for CORS and CSRF on credentialed browser requests, so a wrong # port here shows up as a blocked upload rather than an obvious error. -NEXT_PUBLIC_WEB_URL=http://localhost:3000 +VITNODE_WEB_URL=http://localhost:3000 # Public origin of this API. Used to build absolute upload URLs for the Local # storage adapter, so it must match where the server is reachable. -NEXT_PUBLIC_API_URL=http://localhost:8000 +VITNODE_API_URL=http://localhost:8000 # === CRON Secret for Internal API Calls === CRON_SECRET=your-secure-cron-secret-key diff --git a/apps/api/docker-compose.yml b/apps/api/docker-compose.yml index 46e8c3f89..c50e79baa 100644 --- a/apps/api/docker-compose.yml +++ b/apps/api/docker-compose.yml @@ -13,7 +13,8 @@ services: volumes: - ../../docker/dev:/var/lib/postgresql/data ports: - - "5432:5432" + # Loopback only - these are development containers with a default password. + - "127.0.0.1:5432:5432" networks: - vitnode_dev @@ -23,7 +24,7 @@ services: restart: unless-stopped command: redis-server --requirepass ${REDIS_PASSWORD-root} ports: - - "6379:6379" + - "127.0.0.1:6379:6379" networks: - vitnode_dev diff --git a/apps/api/migrations/20260906174535_canonicalize_user_emails/migration.sql b/apps/api/migrations/20260906174535_canonicalize_user_emails/migration.sql new file mode 100644 index 000000000..2758d6bb1 --- /dev/null +++ b/apps/api/migrations/20260906174535_canonicalize_user_emails/migration.sql @@ -0,0 +1,49 @@ +WITH normalized AS ( + SELECT "id", "email", lower(btrim("email")) AS "normalized" FROM "core_users" +), split AS ( + SELECT + "id", + "email", + "normalized", + CASE WHEN length("normalized") - length(replace("normalized", '@', '')) = 1 + THEN split_part("normalized", '@', 1) END AS "localPart", + CASE WHEN length("normalized") - length(replace("normalized", '@', '')) = 1 + THEN split_part("normalized", '@', 2) END AS "domain" + FROM normalized +), canonical AS ( + SELECT + "id", + "email", + CASE + WHEN "localPart" IS NULL OR "localPart" = '' OR "domain" = '' OR left("localPart", 1) = '"' + THEN "normalized" + WHEN "domain" IN ('gmail.com', 'googlemail.com') + THEN CASE + WHEN replace(split_part("localPart", '+', 1), '.', '') = '' THEN "normalized" + ELSE replace(split_part("localPart", '+', 1), '.', '') || '@gmail.com' + END + WHEN "domain" IN ( + 'fastmail.com', 'fastmail.fm', 'gmx.com', 'gmx.de', 'gmx.net', + 'icloud.com', 'mac.com', 'me.com', 'pm.me', 'proton.me', + 'protonmail.com', 'yandex.com', 'yandex.ru', 'zoho.com' + ) OR "domain" ~ '^(outlook|hotmail|live|msn)\.[a-z]{2,}(\.[a-z]{2,})?$' + THEN CASE + WHEN split_part("localPart", '+', 1) = '' THEN "normalized" + ELSE split_part("localPart", '+', 1) || '@' || "domain" + END + ELSE "normalized" + END AS "canonical" + FROM split +) +UPDATE "core_users" AS u SET "email" = c."canonical" +FROM canonical c +WHERE c."id" = u."id" + AND c."canonical" <> u."email" + AND NOT EXISTS ( + SELECT 1 FROM "core_users" taken + WHERE taken."email" = c."canonical" AND taken."id" <> c."id" + ) + AND NOT EXISTS ( + SELECT 1 FROM canonical rival + WHERE rival."canonical" = c."canonical" AND rival."id" < c."id" + ); diff --git a/apps/api/migrations/20260906174535_canonicalize_user_emails/snapshot.json b/apps/api/migrations/20260906174535_canonicalize_user_emails/snapshot.json new file mode 100644 index 000000000..b820a80cc --- /dev/null +++ b/apps/api/migrations/20260906174535_canonicalize_user_emails/snapshot.json @@ -0,0 +1,7763 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "6f431da8-4f7b-4bc2-bbf8-619d66d0ffea", + "prevIds": [ + "b5e69006-8218-4b5c-b1ce-2d4060b75a34" + ], + "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_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": "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" + }, + { + "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_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": "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": "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": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "authorId", + "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", + "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": "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": "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": "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" + }, + { + "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": "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": "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": "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": "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": "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": "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": "authorId", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "core_search_index_author_id_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": "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": "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": 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": [ + "authorId" + ], + "schemaTo": "public", + "tableTo": "core_users", + "columnsTo": [ + "id" + ], + "onUpdate": "CASCADE", + "onDelete": "SET NULL", + "name": "core_search_index_authorId_core_users_id_fk", + "entityType": "fks", + "schema": "public", + "table": "core_search_index" + }, + { + "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": [ + "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": [ + "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": [ + "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_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": "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" + }, + { + "nameExplicit": true, + "columns": [ + "itemType", + "itemId", + "languageCode" + ], + "nullsNotDistinct": false, + "name": "core_search_index_item_unique", + "entityType": "uniques", + "schema": "public", + "table": "core_search_index" + }, + { + "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": [ + "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" + } + ], + "renames": [] +} diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 5d7ad7916..6df8d5510 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -4,6 +4,8 @@ import { serve, upgradeWebSocket } from "@hono/node-server"; import { serveStatic } from "@hono/node-server/serve-static"; import { OpenAPIHono } from "@hono/zod-openapi"; import { VitNodeAPI } from "@vitnode/core/api/config"; +import { storageStaticHeadersMiddleware } from "@vitnode/core/api/middlewares/storage-static.middleware"; +import { websocketOriginMiddleware } from "@vitnode/core/api/middlewares/websocket-origin.middleware"; import { handleVitNodeWebSocket } from "@vitnode/core/ws/handle"; import { mkdirSync } from "node:fs"; import { WebSocketServer } from "ws"; @@ -24,6 +26,10 @@ if (staticStorage) { mkdirSync(staticStorage.root, { recursive: true }); app.get( staticStorage.mountPath, + // Stored files are served from this origin - the one the session cookie + // belongs to - so anything the browser would treat as a document has to be + // stopped from executing in it. See the middleware. + storageStaticHeadersMiddleware(), serveStatic({ root: staticStorage.root, rewriteRequestPath: path => @@ -37,7 +43,7 @@ if (staticStorage) { // Allow the web frontend to make credentialed browser requests (e.g. client-side // file uploads) when it runs on a different origin than this API. Credentialed // requests can't use a wildcard origin, and CSRF must trust it too. -const webOrigin = process.env.NEXT_PUBLIC_WEB_URL ?? "http://localhost:3000"; +const webOrigin = process.env.VITNODE_WEB_URL ?? "http://localhost:3000"; VitNodeAPI({ app, @@ -48,7 +54,14 @@ VitNodeAPI({ const wss = new WebSocketServer({ noServer: true }); -app.get("/ws", upgradeWebSocket(handleVitNodeWebSocket())); +// The handshake is a cookie-authenticated GET, which Hono's `csrf()` does not +// cover and the same-origin policy does not apply to. Without this any site +// could open a socket as a visiting user - see the middleware. +app.get( + "/ws", + websocketOriginMiddleware({ origin: [webOrigin] }), + upgradeWebSocket(handleVitNodeWebSocket()), +); serve( { diff --git a/apps/web/.env.example b/apps/web/.env.example index dad9d0fa0..692c239d6 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -10,10 +10,10 @@ REDIS_URL=redis://localhost:6379 # `src/server/fetcher.server.ts` takes the origin off the request being # rendered, and in the browser `CONFIG.api` falls back to the origin the page # was served from - so a preview deployment on a generated hostname needs no -# config at all. Set `NEXT_PUBLIC_API_URL` only to point at a separate API -# server; `NEXT_PUBLIC_WEB_URL` is still what the API stamps cookies with. -NEXT_PUBLIC_WEB_URL=http://localhost:3000 -NEXT_PUBLIC_API_URL=http://localhost:3000 +# config at all. Set `VITNODE_API_URL` only to point at a separate API +# server; `VITNODE_WEB_URL` is still what the API stamps cookies with. +VITNODE_WEB_URL=http://localhost:3000 +VITNODE_API_URL=http://localhost:3000 # === CRON Secret for Internal API Calls === CRON_SECRET=your-secure-cron-secret-key diff --git a/apps/web/content/docs/dev/advanced/auth.mdx b/apps/web/content/docs/dev/advanced/auth.mdx index 08d2fb863..4f1684b9c 100644 --- a/apps/web/content/docs/dev/advanced/auth.mdx +++ b/apps/web/content/docs/dev/advanced/auth.mdx @@ -46,9 +46,50 @@ export const vitNodeApiConfig = buildApiConfig({ - **SHA-256 Token Storage**: The raw token is stored only in the user's `HttpOnly` cookie. The database stores only its cryptographic hash. - **Device Pinning**: Tokens are bound to a verified device ID. Tokens copied to another device without the matching device cookie are rejected. - **Fast 60-Second Caching**: Active sessions are cached in Redis for up to 60 seconds, eliminating database query overhead on repeated requests. +- **Canonical Email Identity**: Addresses are stored in their canonical form, so Gmail dot, `googlemail.com` and `+tag` variants cannot become a second account. See [One Mailbox, One Account](#one-mailbox-one-account). --- +## One Mailbox, One Account + +`jankowalski@gmail.com` and `jan.kowalski@gmail.com` look like two addresses. Gmail ignores the dots, so they are one mailbox - and a site that treats them as two lets the same person hold two accounts, dodge a ban, or farm their own referral link. + +VitNode stores the **canonical** form of an address, so those tricks collapse into a single account: + +| Typed at sign-up | Stored in `core_users.email` | +| :--- | :--- | +| `Jan.Kowalski@Gmail.com` | `jankowalski@gmail.com` | +| `jankowalski@googlemail.com` | `jankowalski@gmail.com` | +| `jankowalski+shop@gmail.com` | `jankowalski@gmail.com` | +| `piotr+news@outlook.com` | `piotr@outlook.com` | +| `zofia.nowak@company.com` | `zofia.nowak@company.com` | + +The rules are per-provider, because they are only true per-provider: + +- **Dots are removed for `gmail.com` and `googlemail.com` only.** Everywhere else a dot is part of the mailbox, so `jan.kowalski@company.com` is left alone. +- **`googlemail.com` folds into `gmail.com`.** Google treats them as one domain. +- **`+tag` suffixes are dropped** for Gmail and for the providers that document sub-addressing: Outlook, Hotmail, Live, MSN, iCloud, Proton, Fastmail, GMX, Yandex and Zoho. Mail sent to the canonical address still lands in the same inbox. +- **Everything else is kept as typed**, lowercased. A `+` can be a real character in a corporate address, so guessing there would lock people out. +- **Yahoo's `-` aliases are deliberately left alone**, because `jan-kowalski@yahoo.com` is a perfectly ordinary name. + + + Sign-up, sign-in, password reset, the SSO callback and AdminCP user management all resolve an address the same way. Someone who registered with a password at `jankowalski@gmail.com` and later clicks *Sign in with Google* - which hands back `jan.kowalski@gmail.com` - lands on their own account instead of a second one. + + + + The `20260906174535_canonicalize_user_emails` migration rewrites stored addresses in place. A row whose canonical form is already taken by another row is left untouched, so nobody is locked out and no two accounts are silently merged. Run this before upgrading to see whether your install has such pairs: + + ```sql + SELECT + replace(split_part(split_part(lower("email"), '@', 1), '+', 1), '.', '') AS mailbox, + array_agg("email") + FROM "core_users" + WHERE lower("email") LIKE '%@gmail.com' OR lower("email") LIKE '%@googlemail.com' + GROUP BY 1 + HAVING count(*) > 1; + ``` + + ## `authorization` Options Reference diff --git a/apps/web/content/docs/dev/cron/custom-adapter.mdx b/apps/web/content/docs/dev/cron/custom-adapter.mdx index c492fed9f..4180939ad 100644 --- a/apps/web/content/docs/dev/cron/custom-adapter.mdx +++ b/apps/web/content/docs/dev/cron/custom-adapter.mdx @@ -67,7 +67,7 @@ import { TypeTable } from 'fumadocs-ui/components/type-table' /> `handleCronJobs()` comes from the same module and takes no arguments. It posts to -`/api/@vitnode/core/cron` on the origin in `NEXT_PUBLIC_API_URL` (falling back to +`/api/@vitnode/core/cron` on the origin in `VITNODE_API_URL` (falling back to `http://localhost:3000`) with `Content-Type: application/json` and `Authorization: Bearer $CRON_SECRET`. It resolves as soon as the response arrives and never looks at it, so a `403` or a `500` resolves exactly like a diff --git a/apps/web/content/docs/dev/cron/node-cron.mdx b/apps/web/content/docs/dev/cron/node-cron.mdx index 9ff7d4edd..9c8cb5e6c 100644 --- a/apps/web/content/docs/dev/cron/node-cron.mdx +++ b/apps/web/content/docs/dev/cron/node-cron.mdx @@ -134,7 +134,7 @@ Three consequences worth writing down: | Because the tick is an HTTP call… | …this is true | | ----------------------------------------------------- | -------------------------------------------------------------------------------------- | -| it needs a URL | the origin comes from `NEXT_PUBLIC_API_URL`, falling back to `http://localhost:3000` | +| it needs a URL | the origin comes from `VITNODE_API_URL`, falling back to `http://localhost:3000` | | the API must be able to reach itself over the network | a tick dies where that origin is wrong, unreachable, or fronted by an auth proxy | | the request is authenticated like any other | an unset `CRON_SECRET` means both sides use the well-known default, and the tick works | @@ -142,9 +142,9 @@ Three consequences worth writing down: - `handleCronJobs()` builds its URL from `NEXT_PUBLIC_API_URL` and falls back to + `handleCronJobs()` builds its URL from `VITNODE_API_URL` and falls back to `http://localhost:3000`. This repo's standalone API listens on **8000** - which is exactly why `apps/api/.env.example` sets the variable - so leaving it unset sends every tick to port 3000, where the web app lives. Symptom: the diff --git a/apps/web/content/docs/dev/debugging/index.mdx b/apps/web/content/docs/dev/debugging/index.mdx index 8086c83d1..c8075a035 100644 --- a/apps/web/content/docs/dev/debugging/index.mdx +++ b/apps/web/content/docs/dev/debugging/index.mdx @@ -165,11 +165,13 @@ every time, to both the table and the console. If you want less noise, log less. installation runs. Log events, not traffic. - - The IP on a log row - and the rate-limit bucket - comes from the request's - `X-Forwarded-For` header, falling back to `127.0.0.1` when there is none. A - reverse proxy that does not set it makes every visitor look like the same one. - See the [reverse proxy step](/docs/dev/deployments/self-hosted). + + The IP on a log row - and the rate-limit bucket - is the address of the socket + VitNode is talking to, because `X-Forwarded-For` is a header the caller writes + and nobody should get to pick their own audit trail. Behind a reverse proxy + that socket is the proxy, so every visitor looks like the same one. Put the + per-visitor rate limit in the [proxy + itself](/docs/dev/deployments/self-hosted), where the address is real. diff --git a/apps/web/content/docs/dev/deployments/cloud/vercel.mdx b/apps/web/content/docs/dev/deployments/cloud/vercel.mdx index 7cc42493d..5aaf1fb93 100644 --- a/apps/web/content/docs/dev/deployments/cloud/vercel.mdx +++ b/apps/web/content/docs/dev/deployments/cloud/vercel.mdx @@ -61,15 +61,12 @@ the browser bundle and cannot be changed afterwards without building again. | Variable | Notes | | ---------------------------- | ------------------------------------------------------------------------------------------------------- | | `POSTGRES_URL` | Required. Nothing works without it. | -| `NEXT_PUBLIC_WEB_URL` | Your production origin. Email links, SSO callbacks and password-reset links use it. | +| `VITNODE_WEB_URL` | Your production origin. Email links, SSO callbacks and password-reset links use it. | | `CRON_SECRET` | Required if anything schedules cron. A long random string. | | `REDIS_URL` `REDIS_PASSWORD` | Strongly recommended - see the rate-limiter gotcha below. | -| `NEXT_PUBLIC_API_URL` | Leave it unset. The single app answers `/api/*` itself, and both halves work that out from the request. | +| `VITNODE_API_URL` | Leave it unset. The single app answers `/api/*` itself, and both halves work that out from the request. | - + A preview gets a generated hostname, so no configured value can name it. Left unset, the server takes the API origin off the request being rendered and the browser falls back to the origin the page came from - which is right on every diff --git a/apps/web/content/docs/dev/deployments/self-hosted.mdx b/apps/web/content/docs/dev/deployments/self-hosted.mdx index 7b78c37d3..1782f1cb6 100644 --- a/apps/web/content/docs/dev/deployments/self-hosted.mdx +++ b/apps/web/content/docs/dev/deployments/self-hosted.mdx @@ -55,7 +55,7 @@ POSTGRES_URL=postgresql://user:password@localhost:5432/vitnode_db # The origin visitors reach. Email links, SSO callbacks and password-reset # links are built from it. -NEXT_PUBLIC_WEB_URL=https://example.com +VITNODE_WEB_URL=https://example.com # Required if anything schedules cron. A long random string. CRON_SECRET=change_me_to_a_long_random_secret_string @@ -67,7 +67,7 @@ REDIS_PASSWORD=change_me_redis_password ### Where the API lives -A **Single App** answers `/api/*` itself, so leave `NEXT_PUBLIC_API_URL` unset. +A **Single App** answers `/api/*` itself, so leave `VITNODE_API_URL` unset. The server takes the API origin off the request being rendered and the browser falls back to the origin the page was served from, so the same build is correct on every hostname you point at it. @@ -79,7 +79,7 @@ the session read on the first render. Set it in the web app's environment, and set it before the build: it is compiled into the browser bundle too. ```bash title="apps/web/.env" -NEXT_PUBLIC_API_URL=http://localhost:8000 +VITNODE_API_URL=http://localhost:8000 ``` A value set here wins over the request origin, which is why a Single App must diff --git a/apps/web/content/docs/dev/sso/discord.mdx b/apps/web/content/docs/dev/sso/discord.mdx index 9cabaa6ae..3a28c33e5 100644 --- a/apps/web/content/docs/dev/sso/discord.mdx +++ b/apps/web/content/docs/dev/sso/discord.mdx @@ -69,13 +69,13 @@ authorization screen, and accept the terms. Open **OAuth2** in the left navigation and, under **Redirects**, press **Add Redirect**. Paste the URI for the origin you are running and save. -VitNode builds this URI from `NEXT_PUBLIC_WEB_URL`, so it has to match exactly - +VitNode builds this URI from `VITNODE_WEB_URL`, so it has to match exactly - no trailing slash, no locale prefix. -| Environment | Redirect URL | -| ----------------------------------------- | ------------------------------------------- | -| Development (`NEXT_PUBLIC_WEB_URL` unset) | `http://localhost:3000/login/sso/discord` | -| Production | `https://your-domain.com/login/sso/discord` | +| Environment | Redirect URL | +| ------------------------------------- | ------------------------------------------- | +| Development (`VITNODE_WEB_URL` unset) | `http://localhost:3000/login/sso/discord` | +| Production | `https://your-domain.com/login/sso/discord` | Add both entries so one application covers local development and your live site. @@ -105,9 +105,9 @@ never revealed or you have lost it - Discord shows it once. ### Set the environment variables -Server-side variables, so **no** `NEXT_PUBLIC_` prefix - that prefix is what -marks a value for the browser bundle, and a client secret in the browser bundle -is a client secret you have published. +Server-side variables, so leave them out of `clientEnv` - that list is what +publishes a value to the browser bundle, and a client secret in the browser +bundle is a client secret you have published. ```bash title=".env" DISCORD_CLIENT_ID=1234567890123456789 # [!code ++] diff --git a/apps/web/content/docs/dev/sso/facebook.mdx b/apps/web/content/docs/dev/sso/facebook.mdx index 0e6d68102..d32690b0f 100644 --- a/apps/web/content/docs/dev/sso/facebook.mdx +++ b/apps/web/content/docs/dev/sso/facebook.mdx @@ -102,13 +102,13 @@ Authenticate and request data from users with Facebook Login → Customize → Settings** on a new app, or **Products → Facebook Login → Settings** on an older one. Paste your URI into **Valid OAuth Redirect URIs** and save. -VitNode builds this URI itself, from `NEXT_PUBLIC_WEB_URL`, so it has to match +VitNode builds this URI itself, from `VITNODE_WEB_URL`, so it has to match character for character - no trailing slash, no locale prefix. -| Environment | Valid OAuth Redirect URI | -| ----------------------------------------- | -------------------------------------------- | -| Development (`NEXT_PUBLIC_WEB_URL` unset) | `http://localhost:3000/login/sso/facebook` | -| Production | `https://your-domain.com/login/sso/facebook` | +| Environment | Valid OAuth Redirect URI | +| ------------------------------------- | -------------------------------------------- | +| Development (`VITNODE_WEB_URL` unset) | `http://localhost:3000/login/sso/facebook` | +| Production | `https://your-domain.com/login/sso/facebook` | {/* Image prompt: The Meta app dashboard Facebook Login Settings panel with "Valid OAuth Redirect URIs" holding one entry reading https://your-domain.com/login/sso/facebook, the "Client OAuth login" and "Web OAuth login" toggles visible above it, light theme, 1200x700. */} @@ -139,9 +139,9 @@ text; the **App Secret** is behind a **Show** button and your password. ### Set the environment variables -Server-side variables, so **no** `NEXT_PUBLIC_` prefix - that prefix is what -marks a value for the browser bundle, and an app secret in the browser bundle is -an app secret you have published. +Server-side variables, so leave them out of `clientEnv` - that list is what +publishes a value to the browser bundle, and an app secret in the browser bundle +is an app secret you have published. ```bash title=".env" FACEBOOK_CLIENT_ID=1234567890123456 # [!code ++] diff --git a/apps/web/content/docs/dev/sso/google.mdx b/apps/web/content/docs/dev/sso/google.mdx index b3f1e3e0c..bca939655 100644 --- a/apps/web/content/docs/dev/sso/google.mdx +++ b/apps/web/content/docs/dev/sso/google.mdx @@ -103,13 +103,13 @@ Go to **Clients** in the left navigation and press **Create client**. 2. Name: anything - this one is internal too. 3. Under **Authorized redirect URIs**, add the URI for every origin you run. -VitNode builds this URI itself, from `NEXT_PUBLIC_WEB_URL`, so it has to match +VitNode builds this URI itself, from `VITNODE_WEB_URL`, so it has to match character for character - no trailing slash, no locale prefix. -| Environment | Authorized redirect URI | -| ----------------------------------------- | ------------------------------------------ | -| Development (`NEXT_PUBLIC_WEB_URL` unset) | `http://localhost:3000/login/sso/google` | -| Production | `https://your-domain.com/login/sso/google` | +| Environment | Authorized redirect URI | +| ------------------------------------- | ------------------------------------------ | +| Development (`VITNODE_WEB_URL` unset) | `http://localhost:3000/login/sso/google` | +| Production | `https://your-domain.com/login/sso/google` | Add both. Google allows several redirect URIs per client, and one client for dev and prod is fine while you are getting started. @@ -141,8 +141,8 @@ into `.env`. ### Set the environment variables These names are not magic - they are whatever you read in your config on the -next step. What matters is that they are server-side variables, so **no** -`NEXT_PUBLIC_` prefix: that prefix is what marks a value for the browser bundle, +next step. What matters is that they are server-side variables, so leave them +out of `clientEnv`: that list is what publishes a value to the browser bundle, and a client secret in the browser bundle is a client secret you have published. ```bash title=".env" diff --git a/apps/web/content/docs/dev/sso/index.mdx b/apps/web/content/docs/dev/sso/index.mdx index e96b32c9c..8409f0572 100644 --- a/apps/web/content/docs/dev/sso/index.mdx +++ b/apps/web/content/docs/dev/sso/index.mdx @@ -73,8 +73,11 @@ For example: `https://your-domain.com/login/sso/google`. ## How Accounts Link When a user signs in via SSO: -1. **Existing Email Match**: If an existing account shares the verified email, the SSO identity is automatically linked. -2. **New Visitor**: A new user is created with their social display name, email, and avatar. +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, so the request is refused with `409`. A social login can never take over an account someone else registered with a password. +3. **New Visitor**: A new user is created with their social display name, email, and avatar. + +The address the provider returns is matched in its canonical form, so a Google account that reports `jan.kowalski@gmail.com` finds the member who registered as `jankowalski@gmail.com` instead of quietly becoming a second account. See [One Mailbox, One Account](/docs/dev/advanced/auth#one-mailbox-one-account). ## Learn More diff --git a/apps/web/content/docs/dev/storage/local.mdx b/apps/web/content/docs/dev/storage/local.mdx index a487f3bd6..c3b5f09f0 100644 --- a/apps/web/content/docs/dev/storage/local.mdx +++ b/apps/web/content/docs/dev/storage/local.mdx @@ -168,12 +168,12 @@ The adapter builds absolute URLs as `{baseUrl}/{publicPath}/{key}`. `baseUrl` defaults to the configured API origin, so this env var is what decides whether your file URLs are reachable: -| Variable | Required | What it is for | -| :-------------------- | :---------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `NEXT_PUBLIC_API_URL` | Recommended | The public origin of the API. Used as the default `baseUrl`, so a wrong value produces URLs that 404. Falls back to the browser's own origin, then `http://localhost:3000` | +| Variable | Required | What it is for | +| :---------------- | :---------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `VITNODE_API_URL` | Recommended | The public origin of the API. Used as the default `baseUrl`, so a wrong value produces URLs that 404. Falls back to the browser's own origin, then `http://localhost:3000` | ```bash title=".env" -NEXT_PUBLIC_API_URL=http://localhost:8000 +VITNODE_API_URL=http://localhost:8000 ``` Pass `baseUrl` explicitly if the files are fronted by a different hostname than @@ -212,7 +212,7 @@ A `200` means the mount is right. A `404` with the file present on disk means th }, baseUrl: { description: - 'Origin used to build absolute URLs. Defaults to the configured API origin (`NEXT_PUBLIC_API_URL`).', + 'Origin used to build absolute URLs. Defaults to the configured API origin (`VITNODE_API_URL`).', type: 'string', }, }} diff --git a/apps/web/src/router.tsx b/apps/web/src/router.tsx index 96ae611e0..630d4db3e 100644 --- a/apps/web/src/router.tsx +++ b/apps/web/src/router.tsx @@ -4,9 +4,9 @@ import { createRouter as createTanStackRouter } from '@tanstack/react-router' import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query' import { createVitNodeQueryClient } from '@vitnode/core/lib/query-client' import { + Error500Page, ErrorActions, NotFound, - Error500Page, } from '@vitnode/core/tanstack/layout' import { RoutePendingSpinner } from '@vitnode/core/tanstack/pending' import { diff --git a/apps/web/src/vitnode.api.config.ts b/apps/web/src/vitnode.api.config.ts index d7dda3fe4..bf3b4bff9 100644 --- a/apps/web/src/vitnode.api.config.ts +++ b/apps/web/src/vitnode.api.config.ts @@ -13,6 +13,16 @@ config({ quiet: true }) export const POSTGRES_URL = process.env.POSTGRES_URL ?? 'postgresql://root:root@localhost:5432/vitnode' +/** + * The API this app serves at `/api/*`, identical in shape to the config + * `apps/api` builds. Nothing here is TanStack-specific: the Hono application is + * unchanged, only the runtime that hands it requests is. + * + * Left out on purpose, because each one is a deployment decision rather than + * part of the mount: `email`, `storage`, `ai`, `cron` and the SSO adapters. Add + * them exactly as `apps/api/src/vitnode.api.config.ts` does when this app needs + * them - `buildApiConfig` treats all of them as optional. + */ export const vitNodeApiConfig = buildApiConfig({ plugins: [blogApiPlugin(), exampleApiPlugin()], storage: { diff --git a/packages/config/eslint.react.config.mjs b/packages/config/eslint.react.config.mjs index 0e58e7ec6..355a82ccb 100644 --- a/packages/config/eslint.react.config.mjs +++ b/packages/config/eslint.react.config.mjs @@ -40,29 +40,6 @@ export default [ "react-hooks/exhaustive-deps": "off", "@eslint-react/no-context-provider": "off", "@eslint-react/no-unstable-default-props": "off", - "no-restricted-imports": [ - "error", - { - patterns: [ - { - // VitNode runs on TanStack Start. Next.js is not a dependency of - // any workspace package, so these resolve to nothing - the ban is - // here to fail the lint with an explanation rather than a - // "cannot find module". - group: ["next", "next/*", "next/**"], - message: - "VitNode no longer runs on Next.js. Use `@vitnode/core/tanstack/*` for routing and navigation.", - }, - { - // `use-intl` is the framework-neutral half of next-intl and is a - // direct dependency of @vitnode/core. `next-intl` re-exports it - // plus Next server bindings that no longer have a runtime. - group: ["next-intl", "next-intl/*"], - message: "Import from `use-intl` instead of `next-intl`.", - }, - ], - }, - ], }, }, ]; diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/docker/docker-compose.yml b/packages/create-vitnode-app/copy-of-vitnode-app/docker/docker-compose.yml index 0af0d0381..67650a851 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/docker/docker-compose.yml +++ b/packages/create-vitnode-app/copy-of-vitnode-app/docker/docker-compose.yml @@ -12,7 +12,11 @@ services: volumes: - ./docker/dev:/var/lib/postgresql/data ports: - - '5432:5432' + # Loopback only. These are development containers with a default password + # of `root`, and `'5432:5432'` publishes them on every interface the host + # has - which on a laptop is whatever café or office network it is joined + # to, and on a VPS is the internet. + - '127.0.0.1:5432:5432' networks: - vitnode_dev @@ -22,7 +26,7 @@ services: restart: unless-stopped command: redis-server --requirepass ${REDIS_PASSWORD-root} ports: - - '6379:6379' + - '127.0.0.1:6379:6379' networks: - vitnode_dev diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/monorepo/apps/api/.env.example b/packages/create-vitnode-app/copy-of-vitnode-app/monorepo/apps/api/.env.example index f6eec56d0..6711dc48c 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/monorepo/apps/api/.env.example +++ b/packages/create-vitnode-app/copy-of-vitnode-app/monorepo/apps/api/.env.example @@ -1,7 +1,7 @@ POSTGRES_URL=postgresql://root:root@localhost:5432/vitnode REDIS_URL=redis://localhost:6379 -NEXT_PUBLIC_WEB_URL=http://localhost:3000 +VITNODE_WEB_URL=http://localhost:3000 # === Docker Database Postgres === POSTGRES_USER=root diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/monorepo/apps/web/.env.example b/packages/create-vitnode-app/copy-of-vitnode-app/monorepo/apps/web/.env.example index 600de8dfe..d0cfcd048 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/monorepo/apps/web/.env.example +++ b/packages/create-vitnode-app/copy-of-vitnode-app/monorepo/apps/web/.env.example @@ -1 +1 @@ -NEXT_PUBLIC_API_URL=http://localhost:8000 +VITNODE_API_URL=http://localhost:8000 diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/monorepo/turbo.json b/packages/create-vitnode-app/copy-of-vitnode-app/monorepo/turbo.json index c6fa3bcc8..583609a7e 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/monorepo/turbo.json +++ b/packages/create-vitnode-app/copy-of-vitnode-app/monorepo/turbo.json @@ -17,13 +17,13 @@ "dependsOn": ["^build"], "inputs": ["$TURBO_DEFAULT$", ".env*"], "outputs": [".output/**", "dist/**"], - "env": ["POSTGRES_URL", "NEXT_PUBLIC_API_URL", "NEXT_PUBLIC_WEB_URL"] + "env": ["POSTGRES_URL", "VITNODE_API_URL", "VITNODE_WEB_URL"] }, "dev": { "cache": false, "persistent": true, "inputs": ["$TURBO_DEFAULT$", ".env*"], - "env": ["POSTGRES_URL", "NEXT_PUBLIC_API_URL", "NEXT_PUBLIC_WEB_URL"] + "env": ["POSTGRES_URL", "VITNODE_API_URL", "VITNODE_WEB_URL"] }, "start": { "dependsOn": ["^start"] diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/.env.example b/packages/create-vitnode-app/copy-of-vitnode-app/root/.env.example index 94b0d1e66..47085c750 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/root/.env.example +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/.env.example @@ -1,7 +1,7 @@ POSTGRES_URL=postgresql://root:root@localhost:5432/vitnode REDIS_URL=redis://localhost:6379 -NEXT_PUBLIC_WEB_URL=http://localhost:3000 +VITNODE_WEB_URL=http://localhost:3000 # === CRON Secret for Internal API Calls === CRON_SECRET=your-secure-cron-secret-key diff --git a/packages/create-vitnode-app/copy-of-vitnode-plugin/root/global.d.ts b/packages/create-vitnode-app/copy-of-vitnode-plugin/root/global.d.ts index d23a54ec6..2661cefc7 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-plugin/root/global.d.ts +++ b/packages/create-vitnode-app/copy-of-vitnode-plugin/root/global.d.ts @@ -4,12 +4,12 @@ import coreApi from "@vitnode/core/locales/api/en.json" with { type: "json" }; import core from "@vitnode/core/locales/en.json" with { type: "json" }; import plugin from "./src/locales/en.json" with { type: "json" }; -// Augmenting `use-intl`, not `next-intl`. `AppConfig` is declared by `use-intl`, -// which is what VitNode renders every string through on every host, and a plugin -// is compiled into its own `dist` and imported by whichever app installed it - -// so a type-level dependency on one host's framework is one every installing app -// inherits. `use-intl` must be a direct dependency of this package for the -// reference above to resolve under pnpm's strict `node_modules`. +// `AppConfig` is declared by `use-intl`, which is what VitNode renders every +// string through on every host, and a plugin is compiled into its own `dist` +// and imported by whichever app installed it - so a type-level dependency on +// one host's framework is one every installing app inherits. `use-intl` must be +// a direct dependency of this package for the reference above to resolve under +// pnpm's strict `node_modules`. // // A plugin can render on both sides (UI components and, e.g., emails), so it // types keys against both of core's trees. Add your own server tree here too diff --git a/packages/create-vitnode-app/src/create/create-package-json.ts b/packages/create-vitnode-app/src/create/create-package-json.ts index 927706834..21cb5ac1b 100644 --- a/packages/create-vitnode-app/src/create/create-package-json.ts +++ b/packages/create-vitnode-app/src/create/create-package-json.ts @@ -132,10 +132,10 @@ export const apiScripts = ( * * That line went missing in Stage 17 and it is the regression this file was * fixed for. The reasoning at the time was correct about the half it was looking - * at: `dev` used to be `vitnode init && next dev`, `init` also copied every - * installed plugin's pages into `src/app/` for Next.js to find, and a plugin's - * routes are compiled into `src/plugin-routes.gen.ts` by the app's own Vite - * plugin now - so the *plugin* half of `init` really had nothing left to do. But + * at: `init` also copied every installed plugin's pages into the host app, and a + * plugin's routes are compiled into `src/plugin-routes.gen.ts` by the app's own + * Vite plugin now - so the *plugin* half of `init` really had nothing left to + * do. But * `init` had a second responsibility nobody was auditing, and dropping the whole * command dropped it too: apply pending migrations and seed the roles, * languages and permissions a VitNode installation cannot answer a request diff --git a/packages/create-vitnode-app/src/create/create-vitnode.ts b/packages/create-vitnode-app/src/create/create-vitnode.ts index 9ef7e7a89..6c8e6ce15 100644 --- a/packages/create-vitnode-app/src/create/create-vitnode.ts +++ b/packages/create-vitnode-app/src/create/create-vitnode.ts @@ -84,12 +84,12 @@ export const createVitNode = async ({ * * It was one `Promise.all`, which is a race whenever the two trees share a * path. They shared two: `.gitignore_template` and `.env.example`. The - * overlay's copies were the pre-TanStack ones - ignoring `/.next/` and - * `next-env.d.ts` while missing `.output`, `.nitro` and the generated - * `src/*.gen.ts`, and dropping `NEXT_PUBLIC_API_URL` and `CRON_SECRET` from - * the environment - so which of the two a new project got depended on which - * `cp` finished last. Both are deleted; `root` owns every generic host file - * and `api-single-app` is a true overlay of API-specific additions. + * overlay's copies were stale - missing `.output`, `.nitro` and the + * generated `src/*.gen.ts`, and dropping `VITNODE_API_URL` and + * `CRON_SECRET` from the environment - so which of the two a new project got + * depended on which `cp` finished last. Both are deleted; `root` owns every + * generic host file and `api-single-app` is a true overlay of API-specific + * additions. * * The order stays explicit anyway. There is nothing overlapping left to * decide, but "the overlay is applied over the base" is the contract, and it diff --git a/packages/create-vitnode-app/src/create/scaffold-invariants.test.ts b/packages/create-vitnode-app/src/create/scaffold-invariants.test.ts index 41a4717ce..4f53a5c22 100644 --- a/packages/create-vitnode-app/src/create/scaffold-invariants.test.ts +++ b/packages/create-vitnode-app/src/create/scaffold-invariants.test.ts @@ -37,40 +37,6 @@ const withoutComments = (source: string): string => source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, ""); describe("the generated application", () => { - it("ships no Next.js App Router topology", () => { - expect(allFiles.filter(file => file.includes("[locale]"))).toEqual([]); - expect(allFiles.filter(file => file.includes("@breadcrumb"))).toEqual([]); - expect(allFiles.filter(file => /(^|\/)src\/app(\/|$)/.test(file))).toEqual( - [], - ); - }); - - it("ships no Next.js configuration", () => { - expect( - allFiles.filter(file => /(^|\/)next\.config\.[cm]?[jt]s$/.test(file)), - ).toEqual([]); - // The Proxy is Next-only middleware, and `next-env.d.ts` is written by a - // Next build - both were committed template files. - expect(allFiles.filter(file => file.endsWith("src/proxy.ts"))).toEqual([]); - expect(allFiles.filter(file => file.endsWith("next-env.d.ts"))).toEqual([]); - }); - - it("imports nothing from next or next-intl", () => { - const offenders = [ - ...appFiles.map(file => [appTemplate, file] as const), - ...pluginFiles.map(file => [pluginTemplate, file] as const), - ] - .filter(([, file]) => /\.[cm]?[jt]sx?$/.test(file)) - .filter(([root, file]) => - /(?:from|import)\s*\(?\s*['"]next(?:-intl)?(?:\/[^'"]*)?['"]|declare\s+module\s+['"]next(?:-intl)?['"]|reference\s+types="next/.test( - read(root, file), - ), - ) - .map(([, file]) => file); - - expect(offenders).toEqual([]); - }); - it("is a TanStack Start application", () => { expect(appFiles).toContain("root/vite.config.ts"); expect(appFiles).toContain("root/tsr.config.json"); @@ -147,10 +113,10 @@ describe("what a generated single app starts from", () => { ); /** - * And nothing from the framework this replaced. A fresh scaffold has no + * And nothing from a build this scaffold never runs. A fresh scaffold has no * migration to explain, so these do not belong even as a comment. */ - it.each([".next", "next-env.d.ts", ".contentlayer", ".content-collections"])( + it.each([".contentlayer", ".content-collections"])( "does not mention %s", entry => { expect(gitignore).not.toContain(entry); @@ -159,17 +125,17 @@ describe("what a generated single app starts from", () => { it("ships the single-app environment", () => { expect(env).toContain("POSTGRES_URL="); - expect(env).toContain("NEXT_PUBLIC_WEB_URL=http://localhost:3000"); + expect(env).toContain("VITNODE_WEB_URL=http://localhost:3000"); expect(env).toContain("CRON_SECRET="); }); it("names no API server for an app that serves its own", () => { - expect(env).not.toMatch(/^NEXT_PUBLIC_API_URL=/m); + expect(env).not.toMatch(/^VITNODE_API_URL=/m); }); it("points a split web app at the API's own port", () => { expect(read(appTemplate, "monorepo/apps/web/.env.example")).toContain( - "NEXT_PUBLIC_API_URL=http://localhost:8000", + "VITNODE_API_URL=http://localhost:8000", ); expect(read(appTemplate, "api-bun/src/index.ts")).toContain("port: 8000"); }); @@ -340,14 +306,21 @@ describe("the generated plugin", () => { } }); - it("declares no framework dependency of its own", async () => { - const { versionsPackageJson } = await import("./package-versions.js"); + it("declares no host framework dependency of its own", () => { + // A plugin is compiled into its own `dist` and imported by whichever app + // installed it, so a router in its dependencies is one every installing app + // inherits. `use-intl` is the framework-neutral translator it renders + // through, and is the only i18n dependency it may declare. + const builder = withoutComments( + read(join(packageRoot, "src"), "plugin/create/create-package-json.ts"), + ); + const dependencies = builder.slice( + builder.indexOf("dependencies: {"), + builder.indexOf("devDependencies: {"), + ); - // The version table is what a generated package.json is built from, so a - // framework that is not in it cannot be depended on by accident. - expect(Object.keys(versionsPackageJson)).not.toContain("nextSingle"); - expect(Object.keys(versionsPackageJson)).not.toContain("nextIntl"); - expect(versionsPackageJson.useIntl).toBeTruthy(); + expect(dependencies).toContain('"use-intl": versionsPackageJson.useIntl'); + expect(dependencies).not.toContain("@tanstack/"); }); }); diff --git a/packages/create-vitnode-app/src/plugin/create/route-templates.test.ts b/packages/create-vitnode-app/src/plugin/create/route-templates.test.ts index 0d6e94e49..25531e470 100644 --- a/packages/create-vitnode-app/src/plugin/create/route-templates.test.ts +++ b/packages/create-vitnode-app/src/plugin/create/route-templates.test.ts @@ -103,8 +103,8 @@ describe("the generated route module", () => { ); // A route module is compiled into the plugin's `dist` and imported by - // whichever app installed it, so `next/*`, `next-intl` and a router are all - // ways of making the plugin installable into exactly one kind of app. + // whichever app installed it, so a router import is a way of making the + // plugin installable into exactly one kind of app. expect(imports).toEqual(["use-intl"]); }); diff --git a/packages/vitnode/components.json b/packages/vitnode/components.json index fd7db02db..53bf8f335 100644 --- a/packages/vitnode/components.json +++ b/packages/vitnode/components.json @@ -1,7 +1,7 @@ { "$schema": "https://ui.shadcn.com/schema.json", "style": "base-vega", - "rsc": true, + "rsc": false, "tsx": true, "tailwind": { "config": "", diff --git a/packages/vitnode/eslint.config.mjs b/packages/vitnode/eslint.config.mjs index 21d98098a..f6923d4ee 100644 --- a/packages/vitnode/eslint.config.mjs +++ b/packages/vitnode/eslint.config.mjs @@ -10,18 +10,11 @@ export default [ ...eslintVitNodeReact, { /** - * The Next.js specimen the boundary scanners are pointed at. + * Fixtures a test reads as text. * - * `test-fixtures/next-specimen/` is a deliberate Next.js import graph that - * exists so the "reaches nothing from `next/*`" assertions have a control - - * without it they would all pass vacuously. It is kept outside `src` and - * outside `tsconfig.json`'s `include` on purpose, so `next` never has to be - * installed for tsc to accept the package. - * - * That is exactly why ESLint has to skip it: the typed rules need a file to - * be in the project and this one deliberately is not, and the `next` import - * ban in `@vitnode/config/eslint.react` would flag the specimen for - * containing the very thing it is a specimen of. + * They are kept outside `src` and outside `tsconfig.json`'s `include` on + * purpose, and the typed rules need a file to be in the project - so ESLint + * has to skip them. */ ignores: ["test-fixtures/**"], }, diff --git a/packages/vitnode/scripts/dev.ts b/packages/vitnode/scripts/dev.ts index 4d3254cd8..0ceea6fae 100644 --- a/packages/vitnode/scripts/dev.ts +++ b/packages/vitnode/scripts/dev.ts @@ -24,11 +24,11 @@ const spawnWatch = (command: string, args: string[]) => { /** * `vitnode dev` - a plugin's own build, in watch mode. * - * Three compilers over the plugin's `src/`, and nothing else. Until the Next.js - * cutover this also started a chokidar watcher that copied the plugin's - * `src/routes/{main,admin,blank,breadcrumb}/` into every Next.js app's - * `src/app/[locale]/…` on every save, rewriting each import as it went - so a - * plugin page existed twice and the copy was the one that ran. + * Three compilers over the plugin's `src/`, and nothing else. This also used to + * start a chokidar watcher that copied the plugin's + * `src/routes/{main,admin,blank,breadcrumb}/` into every host app on every save, + * rewriting each import as it went - so a plugin page existed twice and the copy + * was the one that ran. * * An app now reads a plugin's routes out of its `dist` through the generated * route registry, which is why watching `dist` is the whole job: `swc -w` diff --git a/packages/vitnode/scripts/no-route-copier.test.ts b/packages/vitnode/scripts/no-route-copier.test.ts index e2a4e6728..d1f51c9da 100644 --- a/packages/vitnode/scripts/no-route-copier.test.ts +++ b/packages/vitnode/scripts/no-route-copier.test.ts @@ -98,20 +98,6 @@ describe("the plugin route copier", () => { expect(offenders).toEqual([]); }); - - it("copies nothing into an App Router directory", () => { - const offenders = scriptFiles.filter(file => { - const code = codeOf(file); - - return ( - code.includes("[locale]") || - code.includes("@breadcrumb") || - code.includes("(plugins)") - ); - }); - - expect(offenders).toEqual([]); - }); }); describe("the build-time strangler", () => { diff --git a/packages/vitnode/scripts/prepare-database.ts b/packages/vitnode/scripts/prepare-database.ts index cf91e2cc7..025e70437 100644 --- a/packages/vitnode/scripts/prepare-database.ts +++ b/packages/vitnode/scripts/prepare-database.ts @@ -683,9 +683,9 @@ export const databaseBootstrapSteps = ({ * It was `vitnode init` until Stage 17, and the rename is not cosmetic. That * command had two responsibilities bolted together: it prepared the database, * and - through `preparePluginsFiles` - it copied every installed plugin's pages - * into the host app's `src/app/[locale]/…` so Next.js could see them. Removing - * the copier removed the reason `init` was called `init`, and left a command - * whose name promised a project-wide setup while doing one thing. + * into the host app so the host could see them. Removing the copier removed the + * reason `init` was called `init`, and left a command whose name promised a + * project-wide setup while doing one thing. * * A plugin's routes are compiled into a literal registry by the app's own Vite * plugin now, on every dev start and every build. Nothing here prepares a diff --git a/packages/vitnode/src/api/adapters/search/postgres.ts b/packages/vitnode/src/api/adapters/search/postgres.ts index 3a4ee863f..29babf00e 100644 --- a/packages/vitnode/src/api/adapters/search/postgres.ts +++ b/packages/vitnode/src/api/adapters/search/postgres.ts @@ -73,6 +73,16 @@ const buildFilters = (params: SearchQueryParams): SQL | undefined => { return conditions.length ? and(...conditions) : undefined; }; +/** + * The furthest into a relevance-ranked result set a caller may page. + * + * Relevance ordering has no stable key to seek on, so its cursor is an offset - + * and an offset is the one pagination shape whose cost grows with the page + * number. Nobody reaches page ten thousand of a search by reading; they reach it + * by editing the URL. + */ +const MAX_SEARCH_OFFSET = 10_000; + export const PostgresSearchAdapter = (): SearchProviderApiPlugin => ({ name: "postgres", // Two capabilities that are both true for the same reason: this provider's @@ -126,7 +136,17 @@ export const PostgresSearchAdapter = (): SearchProviderApiPlugin => ({ ? sql`ts_rank("core_search_index"."search_vector", websearch_to_tsquery(${config}::regconfig, ${term}))` : sql`NULL`; - const cursorValue = params.cursor ? Number(params.cursor) : undefined; + // Bounded and checked, because on the relevance path this becomes the + // query's `OFFSET`. `Number("abc")` is `NaN`, which Postgres rejects as a + // 500 rather than a bad request, and an unbounded one is a full scan a + // client can ask for by typing a big number into a URL. + const parsedCursor = params.cursor ? Number(params.cursor) : undefined; + const cursorValue = + parsedCursor !== undefined && + Number.isSafeInteger(parsedCursor) && + parsedCursor >= 0 + ? Math.min(parsedCursor, MAX_SEARCH_OFFSET) + : undefined; const orderBy: SQL[] = []; let where = filters; diff --git a/packages/vitnode/src/api/adapters/sso/discord.ts b/packages/vitnode/src/api/adapters/sso/discord.ts index c9a650ab3..b03a968e5 100644 --- a/packages/vitnode/src/api/adapters/sso/discord.ts +++ b/packages/vitnode/src/api/adapters/sso/discord.ts @@ -20,6 +20,7 @@ export const DiscordSSOApiPlugin = ({ id: z.string(), email: z.string(), username: z.string(), + verified: z.boolean(), }); const tokenSchema = z.object({ access_token: z.string(), @@ -78,6 +79,16 @@ export const DiscordSSOApiPlugin = ({ }); } + // As the Google adapter does. An address Discord has not confirmed is an + // address the person signing in may not own, and VitNode keys an account + // on it - so accepting one lets somebody register under an address they + // cannot read, and hold the account the real owner would have had. + if (!data.verified) { + throw new HTTPException(400, { + message: "Email not verified", + }); + } + return data; }, getUrl: ({ state }) => { diff --git a/packages/vitnode/src/api/config.ts b/packages/vitnode/src/api/config.ts index b3c54d4d0..4221744bf 100644 --- a/packages/vitnode/src/api/config.ts +++ b/packages/vitnode/src/api/config.ts @@ -2,6 +2,7 @@ import type { OpenAPIHono } from "@hono/zod-openapi"; import type { Context, Env, Schema } from "hono"; import { swaggerUI } from "@hono/swagger-ui"; +import { bodyLimit } from "hono/body-limit"; import { cors } from "hono/cors"; import { csrf } from "hono/csrf"; import { HTTPException } from "hono/http-exception"; @@ -9,10 +10,12 @@ import { HTTPException } from "hono/http-exception"; import type { VitNodeApiConfig } from "@/vitnode.config"; import { createCacheClient } from "@/api/lib/cache-client"; +import { clientIpMiddleware } from "@/api/lib/client-ip"; import { collectCronJobs } from "@/api/lib/cron"; import { describeError } from "@/api/lib/error-details"; import { newBuildPluginApiCore } from "@/api/plugin"; import { CONFIG_PLUGIN } from "@/config"; +import { CONFIG } from "@/lib/config"; import { initRealtimePubSub } from "@/ws/registry"; import { @@ -22,6 +25,9 @@ import { import { rateLimiterMiddleware } from "./middlewares/rate-limiter.middleware"; import { registerCronJobs } from "./modules/cron/helpers/register-cron-jobs"; +/** 25 MB: room for an image upload, and nothing like enough to be a weapon. */ +const DEFAULT_MAX_BODY_SIZE = 25 * 1024 * 1024; + interface CORSOptions { allowHeaders?: string[]; allowMethods?: string[]; @@ -62,23 +68,48 @@ export function VitNodeAPI({ const plugins = [newBuildPluginApiCore, ...vitNodeApiConfig.plugins]; - app.doc("/swagger/doc", { - openapi: "3.0.0", - info: { - version: CONFIG_PLUGIN.version, - title: "VitNode API", - }, - tags: plugins.flatMap( - plugin => plugin.openApiTags?.map(name => ({ name })) ?? [], - ), - }); + // The generated document names every route, parameter and response shape in + // the install, including the admin tree - a map of the attack surface, handed + // out unauthenticated. Published in development, where it is the point, and + // in production only when an install asks for it via `docs: { enabled: true }`. + const docsEnabled = vitNodeApiConfig.docs?.enabled ?? CONFIG.node_development; + + if (docsEnabled) { + app.doc("/swagger/doc", { + openapi: "3.0.0", + info: { + version: CONFIG_PLUGIN.version, + title: "VitNode API", + }, + tags: plugins.flatMap( + plugin => plugin.openApiTags?.map(name => ({ name })) ?? [], + ), + }); + } + app.use(cors(corsOptions)); app.use(csrf(csrfOptions)); + app.use("*", clientIpMiddleware); + // Nothing bounded a request body before this. `POST /sign_in` reads its JSON + // and then runs scrypt unconditionally, so a body the server is willing to + // buffer is memory *and* CPU an unauthenticated caller gets to choose the size + // of. Uploads are the one thing that legitimately needs room, and they are + // bounded per field by the Content Engine's own `maxBytes`; this is the outer + // wall, and `maxBodySize` moves it for an install that stores large media. + app.use( + "*", + bodyLimit({ + maxSize: vitNodeApiConfig.maxBodySize ?? DEFAULT_MAX_BODY_SIZE, + onError: c => c.json({ error: "Payload Too Large" }, 413), + }), + ); app.use( "*", rateLimiterMiddleware(vitNodeApiConfig.rateLimiter, redisClient), ); - app.get("/swagger", swaggerUI({ url: "/api/swagger/doc" })); + if (docsEnabled) { + app.get("/swagger", swaggerUI({ url: "/api/swagger/doc" })); + } app.use( "*", globalMiddleware({ diff --git a/packages/vitnode/src/api/lib/auth-cookie.test.ts b/packages/vitnode/src/api/lib/auth-cookie.test.ts index d4ecbbada..fe60720f3 100644 --- a/packages/vitnode/src/api/lib/auth-cookie.test.ts +++ b/packages/vitnode/src/api/lib/auth-cookie.test.ts @@ -123,7 +123,7 @@ describe("setAuthCookie", () => { }); it("is opt-in, not derived from anything", () => { - // The regression this closes: a domain guessed from `NEXT_PUBLIC_WEB_URL` + // The regression this closes: a domain guessed from `VITNODE_WEB_URL` // is `localhost` in development and the production domain on a preview // deployment - wrong in both places, and silent. expect( diff --git a/packages/vitnode/src/api/lib/auth-cookie.ts b/packages/vitnode/src/api/lib/auth-cookie.ts index 77863c111..7284ac429 100644 --- a/packages/vitnode/src/api/lib/auth-cookie.ts +++ b/packages/vitnode/src/api/lib/auth-cookie.ts @@ -12,6 +12,14 @@ const authCookieOptions = (c: Context): CookieOptions => { domain: cookieDomain, httpOnly: true, path: "/", + // Stated rather than left to the browser. Chrome and Firefox default an + // omitted `SameSite` to `Lax`, but that is a default and not a rule: Safari + // and older engines have their own, and a cookie whose cross-site behaviour + // depends on which browser is reading it is one nobody can reason about. + // `Lax` and not `Strict` because the SSO round trip lands here as a + // top-level cross-site GET - `Strict` would drop the state cookie on the way + // back from the provider and break every social sign-in. + sameSite: "Lax", secure: cookieSecure, }; }; diff --git a/packages/vitnode/src/api/lib/client-ip.test.ts b/packages/vitnode/src/api/lib/client-ip.test.ts new file mode 100644 index 000000000..680e26a5f --- /dev/null +++ b/packages/vitnode/src/api/lib/client-ip.test.ts @@ -0,0 +1,61 @@ +import { Hono } from "hono"; +import { describe, expect, it } from "vitest"; + +import { clientIpMiddleware } from "./client-ip"; + +interface Env { + Variables: { ipAddress: string }; +} + +const resolve = async ({ + headers, + socket, +}: { + headers?: Record; + socket?: string; +}): Promise => { + const app = new Hono(); + app.use("*", clientIpMiddleware); + app.get("/", c => c.text(c.get("ipAddress"))); + + const res = await app.request( + "/", + { headers }, + socket === undefined + ? undefined + : { incoming: { socket: { remoteAddress: socket } } }, + ); + + return await res.text(); +}; + +describe("clientIpMiddleware", () => { + it("uses the socket address", async () => { + await expect(resolve({ socket: "203.0.113.7" })).resolves.toBe( + "203.0.113.7", + ); + }); + + it("falls back to localhost when the runtime exposes no socket", async () => { + await expect(resolve({})).resolves.toBe("127.0.0.1"); + }); + + it.each([ + "x-forwarded-for", + "x-real-ip", + "cf-connecting-ip", + "true-client-ip", + "client-ip", + "forwarded", + ])("ignores the client-settable %s header", async header => { + await expect( + resolve({ socket: "203.0.113.7", headers: { [header]: "9.9.9.9" } }), + ).resolves.toBe("203.0.113.7"); + }); + + it("ignores a forwarded chain even with no socket to fall back to", async () => { + await expect( + resolve({ headers: { "x-forwarded-for": "9.9.9.9, 203.0.113.7" } }), + ).resolves.toBe("127.0.0.1"); + }); +}); diff --git a/packages/vitnode/src/api/lib/client-ip.ts b/packages/vitnode/src/api/lib/client-ip.ts new file mode 100644 index 000000000..4e5bef29e --- /dev/null +++ b/packages/vitnode/src/api/lib/client-ip.ts @@ -0,0 +1,67 @@ +import type { Context } from "hono"; + +const UNKNOWN_CLIENT_IP = "127.0.0.1"; + +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null; + +const nonEmptyString = (value: unknown): string | undefined => + typeof value === "string" && value.length > 0 ? value : undefined; + +const nodeSocketAddress = ( + env: Record, +): string | undefined => { + const incoming = env.incoming; + if (!isRecord(incoming)) return undefined; + + const socket = incoming.socket; + if (!isRecord(socket)) return undefined; + + return nonEmptyString(socket.remoteAddress); +}; + +const bunSocketAddress = ( + env: Record, + request: Request, +): string | undefined => { + const server = env.server; + if (!isRecord(server)) return undefined; + + const requestIP = server.requestIP; + if (typeof requestIP !== "function") return undefined; + + const info: unknown = (requestIP as (request: Request) => unknown)(request); + + return isRecord(info) ? nonEmptyString(info.address) : undefined; +}; + +const denoSocketAddress = ( + env: Record, +): string | undefined => { + const remoteAddr = env.remoteAddr; + + return isRecord(remoteAddr) ? nonEmptyString(remoteAddr.hostname) : undefined; +}; + +const socketAddress = (c: Context): string | undefined => { + const env: unknown = c.env; + if (!isRecord(env)) return undefined; + + return ( + nodeSocketAddress(env) ?? + bunSocketAddress(env, c.req.raw) ?? + denoSocketAddress(env) + ); +}; + +export const resolveClientIp = (c: Context): string => + socketAddress(c) ?? UNKNOWN_CLIENT_IP; + +export const clientIpMiddleware = async ( + c: Context, + next: () => Promise, +) => { + c.set("ipAddress", resolveClientIp(c)); + + await next(); +}; diff --git a/packages/vitnode/src/api/lib/user-email-lookup.test.ts b/packages/vitnode/src/api/lib/user-email-lookup.test.ts new file mode 100644 index 000000000..03b11cedf --- /dev/null +++ b/packages/vitnode/src/api/lib/user-email-lookup.test.ts @@ -0,0 +1,71 @@ +import type { SQL } from "drizzle-orm"; + +import { PgDialect } from "drizzle-orm/pg-core"; +import { describe, expect, it } from "vitest"; + +import { + emailAliases, + matchesEmail, + pickAccountForEmail, +} from "./user-email-lookup"; + +const dialect = new PgDialect(); + +const compile = (condition: SQL | undefined) => { + if (!condition) throw new Error("Expected a condition."); + + return dialect.sqlToQuery(condition); +}; + +describe("emailAliases", () => { + it("pairs the address as typed with its canonical form", () => { + expect(emailAliases("jan.kowalski@googlemail.com")).toEqual([ + "jan.kowalski@googlemail.com", + "jankowalski@gmail.com", + ]); + }); + + it("does not repeat an address that is already canonical", () => { + expect(emailAliases("zofia.nowak@company.com")).toEqual([ + "zofia.nowak@company.com", + ]); + }); +}); + +describe("matchesEmail", () => { + it("searches the email column for every alias", () => { + const { sql, params } = compile( + matchesEmail("jan.kowalski@googlemail.com"), + ); + + expect(sql).toContain('"email"'); + expect(params).toEqual([ + "jan.kowalski@googlemail.com", + "jankowalski@gmail.com", + ]); + }); +}); + +describe("pickAccountForEmail", () => { + it("returns nothing when no account matched", () => { + expect(pickAccountForEmail([], "jan@gmail.com")).toBeUndefined(); + }); + + it("prefers the account whose stored address is the one that was typed", () => { + const rows = [ + { id: 1, email: "jankowalski@gmail.com" }, + { id: 2, email: "jan.kowalski@gmail.com" }, + ]; + + expect(pickAccountForEmail(rows, "jan.kowalski@gmail.com")?.id).toBe(2); + expect(pickAccountForEmail(rows, "jankowalski@gmail.com")?.id).toBe(1); + }); + + it("falls back to the canonical match when the typed address is nobody's", () => { + const rows = [{ id: 1, email: "jankowalski@gmail.com" }]; + + expect( + pickAccountForEmail(rows, "jan.kowalski+ref@googlemail.com")?.id, + ).toBe(1); + }); +}); diff --git a/packages/vitnode/src/api/lib/user-email-lookup.ts b/packages/vitnode/src/api/lib/user-email-lookup.ts new file mode 100644 index 000000000..1fb9dfdc3 --- /dev/null +++ b/packages/vitnode/src/api/lib/user-email-lookup.ts @@ -0,0 +1,26 @@ +import { inArray } from "drizzle-orm"; + +import { core_users } from "@/database/users"; +import { + canonicalizeEmail, + normalizeEmailAddress, +} from "@/lib/email-canonical"; + +export const emailAliases = (email: string): string[] => { + const normalized = normalizeEmailAddress(email); + const canonical = canonicalizeEmail(normalized); + + return canonical === normalized ? [normalized] : [normalized, canonical]; +}; + +export const matchesEmail = (email: string) => + inArray(core_users.email, emailAliases(email)); + +export const pickAccountForEmail = ( + rows: T[], + email: string, +): T | undefined => { + const normalized = normalizeEmailAddress(email); + + return rows.find(row => row.email === normalized) ?? rows[0]; +}; diff --git a/packages/vitnode/src/api/middlewares/captcha.middleware.ts b/packages/vitnode/src/api/middlewares/captcha.middleware.ts index 09628d4c5..40a0cc947 100644 --- a/packages/vitnode/src/api/middlewares/captcha.middleware.ts +++ b/packages/vitnode/src/api/middlewares/captcha.middleware.ts @@ -13,13 +13,22 @@ const getResFromReCaptcha = async ({ token: string; userIp: string; }): Promise<{ "error-codes"?: string[]; score: number; success: boolean }> => { + // An install that configured a captcha but no secret key cannot verify + // anything. Said here rather than left to the provider to reject a malformed + // request: the answer is the same either way, but only one of them is a + // decision this code made on purpose. + const { secretKey } = captchaConfig; + if (!secretKey) { + return { success: false, score: 0, "error-codes": ["missing-secret-key"] }; + } + if (captchaConfig.type === "cloudflare_turnstile") { const res = await fetch( "https://challenges.cloudflare.com/turnstile/v0/siteverify", { method: "POST", body: JSON.stringify({ - secret: captchaConfig.secretKey, + secret: secretKey, response: token, remoteip: userIp, }), @@ -41,12 +50,22 @@ const getResFromReCaptcha = async ({ }; } if (captchaConfig.type === "recaptcha_v3") { - const res = await fetch( - `https://www.google.com/recaptcha/api/siteverify?secret=${captchaConfig.secretKey}&response=${token}&remoteip=${userIp}`, - { - method: "POST", + // Form-encoded body rather than a query string. Interpolating the + // client-supplied token straight into the URL let it carry `&` and add + // parameters of its own to the request - and it put the site's secret key in + // a URL, which is the part of a request that ends up in proxy logs and + // error reports. + const res = await fetch("https://www.google.com/recaptcha/api/siteverify", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", }, - ); + body: new URLSearchParams({ + secret: secretKey, + response: token, + remoteip: userIp, + }), + }); const data: { "error-codes"?: string[]; diff --git a/packages/vitnode/src/api/middlewares/cron-auth.middleware.test.ts b/packages/vitnode/src/api/middlewares/cron-auth.middleware.test.ts new file mode 100644 index 000000000..f363ccf42 --- /dev/null +++ b/packages/vitnode/src/api/middlewares/cron-auth.middleware.test.ts @@ -0,0 +1,124 @@ +import { Hono } from "hono"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { + INSECURE_CRON_SECRETS, + INSECURE_DEFAULT_CRON_SECRET, +} from "@/lib/config"; + +import { cronAuthMiddleware } from "./cron-auth.middleware"; + +interface Env { + Variables: { core: { cronSecret?: string } }; +} + +const buildApp = (cronSecret: string | undefined) => { + const app = new Hono(); + app.use("*", async (c, next) => { + c.set("core", { cronSecret }); + + return next(); + }); + app.use("*", cronAuthMiddleware()); + app.post("/", c => c.text("ran")); + + return app; +}; + +const post = async ( + cronSecret: string | undefined, + authorization?: string, +): Promise => + await buildApp(cronSecret).request("/", { + method: "POST", + headers: authorization === undefined ? {} : { authorization }, + }); + +describe("cronAuthMiddleware", () => { + beforeEach(() => { + vi.stubEnv("NODE_ENV", "production"); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("runs the job for the configured secret", async () => { + const res = await post("s3cret-value", "Bearer s3cret-value"); + + expect(res.status).toBe(200); + }); + + it("refuses a wrong secret", async () => { + const res = await post("s3cret-value", "Bearer wrong"); + + expect(res.status).toBe(403); + }); + + it("refuses a missing authorization header", async () => { + const res = await post("s3cret-value"); + + expect(res.status).toBe(403); + }); + + it("refuses when no secret is configured at all", async () => { + const res = await post(undefined, "Bearer anything"); + + expect(res.status).toBe(403); + }); + + describe("the built-in default secret", () => { + it("is refused in production", async () => { + // The regression this guards: `CONFIG.cronJobSecret` falls back to a + // constant published in this repository, so an install that never set + // `CRON_SECRET` would run every registered cron job for anyone who read + // the source. + const res = await post( + INSECURE_DEFAULT_CRON_SECRET, + `Bearer ${INSECURE_DEFAULT_CRON_SECRET}`, + ); + + expect(res.status).toBe(403); + expect(await res.text()).toContain("CRON_SECRET"); + }); + + it.each([...INSECURE_CRON_SECRETS])( + "refuses the published placeholder %s in production", + async secret => { + // The `.env.example` the scaffolder ships carries its own placeholder, + // so recognising only the code fallback left every install that copied + // that file and never edited the line just as open. + const res = await post(secret, `Bearer ${secret}`); + + expect(res.status).toBe(403); + }, + ); + + it("still works in development", async () => { + vi.stubEnv("NODE_ENV", "development"); + const res = await post( + INSECURE_DEFAULT_CRON_SECRET, + `Bearer ${INSECURE_DEFAULT_CRON_SECRET}`, + ); + + expect(res.status).toBe(200); + }); + }); + + describe("header parsing", () => { + it("does not accept `Bearer` appearing mid-header", async () => { + // `authHeader.replace("Bearer ", "")` used to strip the first occurrence + // wherever it sat, so this parsed as the secret. + const res = await post("s3cret-value", "Basic Bearer s3cret-value"); + + expect(res.status).toBe(403); + }); + + it("does not mangle a secret containing the scheme name", async () => { + const secret = "a Bearer b"; + const res = await post(secret, `Bearer ${secret}`); + + expect(res.status).toBe(200); + }); + }); +}); diff --git a/packages/vitnode/src/api/middlewares/cron-auth.middleware.ts b/packages/vitnode/src/api/middlewares/cron-auth.middleware.ts index 556f4b705..29c05d920 100644 --- a/packages/vitnode/src/api/middlewares/cron-auth.middleware.ts +++ b/packages/vitnode/src/api/middlewares/cron-auth.middleware.ts @@ -1,6 +1,44 @@ import type { Context, Next } from "hono"; import { HTTPException } from "hono/http-exception"; +import { timingSafeEqual } from "node:crypto"; + +import { CONFIG, INSECURE_CRON_SECRETS } from "@/lib/config"; + +/** + * Constant-time comparison of two secrets. + * + * `timingSafeEqual` throws on a length mismatch, so the lengths are compared + * first - and then both branches still run a comparison, because returning + * early on a length difference is itself a signal about the secret. + */ +const secretsMatch = (provided: string, expected: string): boolean => { + const a = Buffer.from(provided, "utf8"); + const b = Buffer.from(expected, "utf8"); + + if (a.length !== b.length) { + timingSafeEqual(b, b); + + return false; + } + + return timingSafeEqual(a, b); +}; + +/** + * `Bearer `, or nothing. + * + * Matched as a *prefix* rather than stripped with `replace`, which removed the + * first `"Bearer "` found anywhere in the header - so `"x Bearer y"` parsed as a + * credential, and a secret that happened to contain the word lost part of + * itself. + */ +const bearerToken = (header: string | undefined): string | undefined => { + if (!header) return undefined; + const match = /^Bearer (.+)$/.exec(header); + + return match?.[1]; +}; export const cronAuthMiddleware = () => { return async (c: Context, next: Next) => { @@ -9,10 +47,25 @@ export const cronAuthMiddleware = () => { throw new HTTPException(403, { message: "Cron access not configured" }); } - const authHeader = c.req.header("authorization"); - const providedSecret = authHeader?.replace("Bearer ", ""); + // `CONFIG.cronJobSecret` falls back to a published constant so that a fresh + // checkout runs its cron jobs without configuration. Outside development + // that fallback is not a weak secret, it is *no* secret: the value is in the + // repository, so anyone could post to this endpoint and run every registered + // job. The admin panel flags it, but a warning nobody reads is not a control, + // so production refuses the request outright. + if ( + INSECURE_CRON_SECRETS.includes(cronSecret) && + !CONFIG.node_development + ) { + throw new HTTPException(403, { + message: + "Cron access is disabled because CRON_SECRET is still the built-in default. Set CRON_SECRET to a random value.", + }); + } + + const providedSecret = bearerToken(c.req.header("authorization")); - if (providedSecret !== cronSecret) { + if (!providedSecret || !secretsMatch(providedSecret, cronSecret)) { throw new HTTPException(403, { message: "Invalid cron authorization" }); } diff --git a/packages/vitnode/src/api/middlewares/global.middleware.ts b/packages/vitnode/src/api/middlewares/global.middleware.ts index 5a0cab439..6cfb70adc 100644 --- a/packages/vitnode/src/api/middlewares/global.middleware.ts +++ b/packages/vitnode/src/api/middlewares/global.middleware.ts @@ -46,6 +46,7 @@ import type { } from "../models/search"; import type { SSOApiPlugin } from "../models/sso"; +import { resolveClientIp } from "../lib/client-ip"; import { collectCronJobs } from "../lib/cron"; import { loggerMiddleware, @@ -185,7 +186,9 @@ export const globalMiddleware = ({ | "search" | "storage" > & - Pick & { cacheClient: CacheClient | null }) => { + Pick & { + cacheClient: CacheClient | null; + }) => { const pluginsMetadata = plugins.map(plugin => ({ id: plugin.pluginId, })); @@ -312,38 +315,10 @@ export const globalMiddleware = ({ }), ); - const ipHeaderKeys = [ - "x-forwarded-for", - "x-real-ip", - "cf-connecting-ip", - "x-client-ip", - "x-forwarded", - "x-cluster-client-ip", - "forwarded-for", - "forwarded", - "via", - "remote-addr", - "client-ip", - "ip", - "x-ip", - "true-client-ip", - "fastly-client-ip", - "x-fastly-client-ip", - ]; - return async (c: Context, next: Next) => { - let ipAddress: string | undefined; - - for (const key of ipHeaderKeys) { - ipAddress = c.req.header(key); - if (ipAddress) break; - - ipAddress = c.req.raw.headers.get(key) ?? undefined; - if (ipAddress) break; + if (!c.get("ipAddress")) { + c.set("ipAddress", resolveClientIp(c)); } - - // Fallback to localhost if nothing found - c.set("ipAddress", ipAddress ?? "127.0.0.1"); c.set("db", dbProvider); c.set("ai", new AIModel(c)); c.set("cache", new CacheModel(cacheClient, c)); diff --git a/packages/vitnode/src/api/middlewares/storage-static.middleware.ts b/packages/vitnode/src/api/middlewares/storage-static.middleware.ts new file mode 100644 index 000000000..73530f871 --- /dev/null +++ b/packages/vitnode/src/api/middlewares/storage-static.middleware.ts @@ -0,0 +1,33 @@ +import type { Context, Next } from "hono"; + +/** + * Headers for the route that serves stored uploads off disk. + * + * Uploads are served from the API's own origin, which is the origin the session + * cookie belongs to. That makes any stored file the browser is willing to treat + * as a *document* - HTML, an SVG carrying a `