From 925da4a661e297ac48c06583ed20d1eb9edf60a1 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Thu, 24 Sep 2026 18:23:20 -0500 Subject: [PATCH] feat: move sso and ldap logins into plugins (C2) --- drizzle/mysql/0052_illegal_gladiator.sql | 4 + drizzle/mysql/meta/0052_snapshot.json | 5457 +++++++++++++++++ drizzle/mysql/meta/_journal.json | 7 + drizzle/postgres/0053_keen_thanos.sql | 4 + drizzle/postgres/meta/0053_snapshot.json | 5331 ++++++++++++++++ drizzle/postgres/meta/_journal.json | 7 + drizzle/sqlite/0049_glamorous_hemingway.sql | 4 + drizzle/sqlite/meta/0049_snapshot.json | 5154 ++++++++++++++++ drizzle/sqlite/meta/_journal.json | 7 + knip.json | 1 - package-lock.json | 34 +- package.json | 3 - packages/plugin-sdk/ARCHITECTURE.md | 83 +- packages/plugin-sdk/FINISH-LIST.md | 33 + packages/plugin-sdk/src/backend.ts | 78 + packages/plugin-sdk/src/testing.ts | 114 + plugins/ldap/locales/en.json | 50 + plugins/ldap/locales/translated/af_ZA.json | 33 + plugins/ldap/locales/translated/ar_SA.json | 33 + plugins/ldap/locales/translated/bg_BG.json | 33 + plugins/ldap/locales/translated/bn_BD.json | 33 + plugins/ldap/locales/translated/ca_ES.json | 33 + plugins/ldap/locales/translated/cs_CZ.json | 33 + plugins/ldap/locales/translated/da_DK.json | 33 + plugins/ldap/locales/translated/de_DE.json | 33 + plugins/ldap/locales/translated/el_GR.json | 33 + plugins/ldap/locales/translated/es_ES.json | 33 + plugins/ldap/locales/translated/fi_FI.json | 33 + plugins/ldap/locales/translated/fr_FR.json | 33 + plugins/ldap/locales/translated/he_IL.json | 33 + plugins/ldap/locales/translated/hi_IN.json | 33 + plugins/ldap/locales/translated/hu_HU.json | 33 + plugins/ldap/locales/translated/id_ID.json | 33 + plugins/ldap/locales/translated/it_IT.json | 33 + plugins/ldap/locales/translated/ja_JP.json | 33 + plugins/ldap/locales/translated/ko_KR.json | 33 + plugins/ldap/locales/translated/nl_NL.json | 33 + plugins/ldap/locales/translated/no_NO.json | 33 + plugins/ldap/locales/translated/pl_PL.json | 33 + plugins/ldap/locales/translated/pt_BR.json | 33 + plugins/ldap/locales/translated/pt_PT.json | 33 + plugins/ldap/locales/translated/ro_RO.json | 33 + plugins/ldap/locales/translated/ru_RU.json | 33 + plugins/ldap/locales/translated/sr_SP.json | 33 + plugins/ldap/locales/translated/sv_SE.json | 33 + plugins/ldap/locales/translated/th_TH.json | 33 + plugins/ldap/locales/translated/tr_TR.json | 33 + plugins/ldap/locales/translated/uk_UA.json | 33 + plugins/ldap/locales/translated/vi_VN.json | 33 + plugins/ldap/locales/translated/zh_CN.json | 33 + plugins/ldap/locales/translated/zh_TW.json | 33 + plugins/ldap/manifest.json | 49 + .../mysql/0001_create_providers.sql | 12 + .../postgres/0001_create_providers.sql | 12 + plugins/ldap/migrations/snapshot.json | 43 + .../sqlite/0001_create_providers.sql | 12 + plugins/ldap/package.json | 19 + plugins/ldap/src/backend/index.ts | 34 + plugins/ldap/src/backend/ldap.ts | 205 + plugins/ldap/src/backend/providers.ts | 141 + plugins/ldap/src/backend/routes.ts | 265 + plugins/ldap/src/backend/tables.ts | 24 + plugins/ldap/src/backend/types.ts | 34 + plugins/ldap/src/frontend/LdapLoginForms.tsx | 101 + .../ldap/src/frontend/ProvidersSetting.tsx | 439 ++ plugins/ldap/src/frontend/index.tsx | 12 + plugins/ldap/src/frontend/ldap-api.ts | 35 + plugins/ldap/tests/backend/helpers.ts | 125 + plugins/ldap/tests/backend/ldap.test.ts | 304 + plugins/ldap/tests/frontend/activate.test.tsx | 93 + plugins/ldap/tsconfig.json | 4 + plugins/ldap/vitest.config.ts | 3 + plugins/sso/locales/en.json | 59 + plugins/sso/locales/translated/af_ZA.json | 41 + plugins/sso/locales/translated/ar_SA.json | 41 + plugins/sso/locales/translated/bg_BG.json | 41 + plugins/sso/locales/translated/bn_BD.json | 41 + plugins/sso/locales/translated/ca_ES.json | 41 + plugins/sso/locales/translated/cs_CZ.json | 41 + plugins/sso/locales/translated/da_DK.json | 41 + plugins/sso/locales/translated/de_DE.json | 41 + plugins/sso/locales/translated/el_GR.json | 41 + plugins/sso/locales/translated/es_ES.json | 41 + plugins/sso/locales/translated/fi_FI.json | 41 + plugins/sso/locales/translated/fr_FR.json | 41 + plugins/sso/locales/translated/he_IL.json | 41 + plugins/sso/locales/translated/hi_IN.json | 41 + plugins/sso/locales/translated/hu_HU.json | 41 + plugins/sso/locales/translated/id_ID.json | 41 + plugins/sso/locales/translated/it_IT.json | 41 + plugins/sso/locales/translated/ja_JP.json | 41 + plugins/sso/locales/translated/ko_KR.json | 41 + plugins/sso/locales/translated/nl_NL.json | 41 + plugins/sso/locales/translated/no_NO.json | 41 + plugins/sso/locales/translated/pl_PL.json | 41 + plugins/sso/locales/translated/pt_BR.json | 41 + plugins/sso/locales/translated/pt_PT.json | 41 + plugins/sso/locales/translated/ro_RO.json | 41 + plugins/sso/locales/translated/ru_RU.json | 41 + plugins/sso/locales/translated/sr_SP.json | 41 + plugins/sso/locales/translated/sv_SE.json | 41 + plugins/sso/locales/translated/th_TH.json | 41 + plugins/sso/locales/translated/tr_TR.json | 41 + plugins/sso/locales/translated/uk_UA.json | 41 + plugins/sso/locales/translated/vi_VN.json | 41 + plugins/sso/locales/translated/zh_CN.json | 41 + plugins/sso/locales/translated/zh_TW.json | 41 + plugins/sso/manifest.json | 50 + .../mysql/0001_adopt_sso_providers.sql | 14 + .../migrations/mysql/0002_legacy_callback.sql | 7 + .../postgres/0001_adopt_sso_providers.sql | 14 + .../postgres/0002_legacy_callback.sql | 7 + plugins/sso/migrations/snapshot.json | 53 + .../sqlite/0001_adopt_sso_providers.sql | 14 + .../sqlite/0002_legacy_callback.sql | 7 + plugins/sso/package.json | 19 + plugins/sso/src/backend/index.ts | 39 + plugins/sso/src/backend/login.ts | 482 ++ .../sso/src/backend/oidc-protocol.ts | 303 +- plugins/sso/src/backend/providers.ts | 278 + plugins/sso/src/backend/routes.ts | 502 ++ plugins/sso/src/backend/tables.ts | 32 + plugins/sso/src/backend/types.ts | 46 + plugins/sso/src/frontend/OidcLoginButtons.tsx | 39 + plugins/sso/src/frontend/ProvidersSetting.tsx | 580 ++ plugins/sso/src/frontend/index.tsx | 12 + plugins/sso/src/frontend/sso-api.ts | 45 + plugins/sso/tests/backend/helpers.ts | 220 + .../sso/tests/backend/oidc-protocol.test.ts | 94 +- plugins/sso/tests/backend/sso.test.ts | 423 ++ plugins/sso/tests/frontend/activate.test.tsx | 112 + plugins/sso/tsconfig.json | 4 + plugins/sso/vitest.config.ts | 3 + src/backend/auth/allowed-users.ts | 44 + src/backend/auth/core-auth.ts | 2 - src/backend/auth/legacy-providers.ts | 71 - src/backend/auth/legacy/ldap-login.ts | 227 - src/backend/auth/legacy/oidc-login.ts | 518 -- src/backend/auth/provisioning.ts | 6 +- src/backend/database/db/index.ts | 45 - src/backend/database/db/schema.mysql.ts | 15 - src/backend/database/db/schema.pg.ts | 15 - src/backend/database/db/schema.ts | 15 - src/backend/database/repositories/factory.ts | 8 - .../repositories/sso-provider-repository.ts | 126 - .../repositories/user-auth-repository.ts | 8 + .../database/routes/auth-compat-routes.ts | 165 + src/backend/database/routes/auth-routes.ts | 220 +- .../database/routes/ldap-auth-routes.ts | 54 - .../database/routes/sso-provider-routes.ts | 488 -- src/backend/database/routes/users.ts | 479 +- src/backend/hosts/credential-username.ts | 22 +- src/backend/plugins/ctx-ssh-auth.ts | 125 +- src/backend/plugins/ctx.ts | 5 + src/backend/plugins/loader.ts | 8 +- src/backend/starter.ts | 31 +- src/backend/tests/auth/allowed-users.test.ts | 70 + src/backend/tests/auth/auth-test-helpers.ts | 10 +- src/backend/tests/auth/legacy-methods.test.ts | 425 -- src/backend/tests/auth/login-pipeline.test.ts | 15 +- .../sso-provider-repository.test.ts | 104 - .../routes/auth-compat-routes.test.ts | 134 + .../routes/sso-provider-routes.test.ts | 24 - .../routes/user-oidc-account-routes.test.ts | 210 + .../tests/hosts/credential-username.test.ts | 18 +- .../tests/plugins/ctx-auth-login.test.ts | 450 ++ .../external-identity-migration.test.ts | 6 +- .../utils/ldap-provider-migration.test.ts | 179 + .../tests/utils/oidc-desktop-callback.test.ts | 20 +- .../tests/utils/system-secret-crypto.test.ts | 62 +- .../external-identity-migration.ts | 18 +- .../ldap-provider-migration.ts | 145 + .../crypto-migration/plugin-data-moves.ts | 12 + src/backend/utils/oidc-desktop-callback.ts | 14 +- src/backend/utils/swagger.ts | 4 - src/backend/utils/system-secret-crypto.ts | 34 - src/types/index.ts | 51 - src/types/ui-types.ts | 1 - src/ui/api/sso-provider-api.ts | 76 - src/ui/api/user-management-api.ts | 20 - src/ui/auth/legacy-auth-ui.tsx | 153 +- src/ui/locales/en.json | 61 - src/ui/locales/translated/af_ZA.json | 59 - src/ui/locales/translated/ar_SA.json | 59 - src/ui/locales/translated/bg_BG.json | 59 - src/ui/locales/translated/bn_BD.json | 59 - src/ui/locales/translated/ca_ES.json | 59 - src/ui/locales/translated/cs_CZ.json | 59 - src/ui/locales/translated/da_DK.json | 59 - src/ui/locales/translated/de_DE.json | 59 - src/ui/locales/translated/el_GR.json | 59 - src/ui/locales/translated/es_ES.json | 59 - src/ui/locales/translated/fi_FI.json | 59 - src/ui/locales/translated/fr_FR.json | 59 - src/ui/locales/translated/he_IL.json | 59 - src/ui/locales/translated/hi_IN.json | 59 - src/ui/locales/translated/hu_HU.json | 59 - src/ui/locales/translated/id_ID.json | 59 - src/ui/locales/translated/it_IT.json | 59 - src/ui/locales/translated/ja_JP.json | 59 - src/ui/locales/translated/ko_KR.json | 59 - src/ui/locales/translated/nl_NL.json | 59 - src/ui/locales/translated/no_NO.json | 59 - src/ui/locales/translated/pl_PL.json | 59 - src/ui/locales/translated/pt_BR.json | 59 - src/ui/locales/translated/pt_PT.json | 59 - src/ui/locales/translated/ro_RO.json | 59 - src/ui/locales/translated/ru_RU.json | 59 - src/ui/locales/translated/sr_SP.json | 59 - src/ui/locales/translated/sv_SE.json | 59 - src/ui/locales/translated/th_TH.json | 59 - src/ui/locales/translated/tr_TR.json | 59 - src/ui/locales/translated/uk_UA.json | 59 - src/ui/locales/translated/vi_VN.json | 59 - src/ui/locales/translated/zh_CN.json | 59 - src/ui/locales/translated/zh_TW.json | 59 - src/ui/main-axios.ts | 46 - src/ui/sidebar/AdminSettingsPanel.tsx | 90 +- src/ui/sidebar/AdminSettingsSections.tsx | 384 +- src/ui/sidebar/SSOProviderDialog.tsx | 726 --- src/ui/tests/auth/Auth.test.tsx | 8 - .../auth/AuthEmbeddedServerFailure.test.tsx | 6 - src/ui/tests/auth/AuthLoginMethods.test.tsx | 8 +- 223 files changed, 25778 insertions(+), 6820 deletions(-) create mode 100644 drizzle/mysql/0052_illegal_gladiator.sql create mode 100644 drizzle/mysql/meta/0052_snapshot.json create mode 100644 drizzle/postgres/0053_keen_thanos.sql create mode 100644 drizzle/postgres/meta/0053_snapshot.json create mode 100644 drizzle/sqlite/0049_glamorous_hemingway.sql create mode 100644 drizzle/sqlite/meta/0049_snapshot.json create mode 100644 plugins/ldap/locales/en.json create mode 100644 plugins/ldap/locales/translated/af_ZA.json create mode 100644 plugins/ldap/locales/translated/ar_SA.json create mode 100644 plugins/ldap/locales/translated/bg_BG.json create mode 100644 plugins/ldap/locales/translated/bn_BD.json create mode 100644 plugins/ldap/locales/translated/ca_ES.json create mode 100644 plugins/ldap/locales/translated/cs_CZ.json create mode 100644 plugins/ldap/locales/translated/da_DK.json create mode 100644 plugins/ldap/locales/translated/de_DE.json create mode 100644 plugins/ldap/locales/translated/el_GR.json create mode 100644 plugins/ldap/locales/translated/es_ES.json create mode 100644 plugins/ldap/locales/translated/fi_FI.json create mode 100644 plugins/ldap/locales/translated/fr_FR.json create mode 100644 plugins/ldap/locales/translated/he_IL.json create mode 100644 plugins/ldap/locales/translated/hi_IN.json create mode 100644 plugins/ldap/locales/translated/hu_HU.json create mode 100644 plugins/ldap/locales/translated/id_ID.json create mode 100644 plugins/ldap/locales/translated/it_IT.json create mode 100644 plugins/ldap/locales/translated/ja_JP.json create mode 100644 plugins/ldap/locales/translated/ko_KR.json create mode 100644 plugins/ldap/locales/translated/nl_NL.json create mode 100644 plugins/ldap/locales/translated/no_NO.json create mode 100644 plugins/ldap/locales/translated/pl_PL.json create mode 100644 plugins/ldap/locales/translated/pt_BR.json create mode 100644 plugins/ldap/locales/translated/pt_PT.json create mode 100644 plugins/ldap/locales/translated/ro_RO.json create mode 100644 plugins/ldap/locales/translated/ru_RU.json create mode 100644 plugins/ldap/locales/translated/sr_SP.json create mode 100644 plugins/ldap/locales/translated/sv_SE.json create mode 100644 plugins/ldap/locales/translated/th_TH.json create mode 100644 plugins/ldap/locales/translated/tr_TR.json create mode 100644 plugins/ldap/locales/translated/uk_UA.json create mode 100644 plugins/ldap/locales/translated/vi_VN.json create mode 100644 plugins/ldap/locales/translated/zh_CN.json create mode 100644 plugins/ldap/locales/translated/zh_TW.json create mode 100644 plugins/ldap/manifest.json create mode 100644 plugins/ldap/migrations/mysql/0001_create_providers.sql create mode 100644 plugins/ldap/migrations/postgres/0001_create_providers.sql create mode 100644 plugins/ldap/migrations/snapshot.json create mode 100644 plugins/ldap/migrations/sqlite/0001_create_providers.sql create mode 100644 plugins/ldap/package.json create mode 100644 plugins/ldap/src/backend/index.ts create mode 100644 plugins/ldap/src/backend/ldap.ts create mode 100644 plugins/ldap/src/backend/providers.ts create mode 100644 plugins/ldap/src/backend/routes.ts create mode 100644 plugins/ldap/src/backend/tables.ts create mode 100644 plugins/ldap/src/backend/types.ts create mode 100644 plugins/ldap/src/frontend/LdapLoginForms.tsx create mode 100644 plugins/ldap/src/frontend/ProvidersSetting.tsx create mode 100644 plugins/ldap/src/frontend/index.tsx create mode 100644 plugins/ldap/src/frontend/ldap-api.ts create mode 100644 plugins/ldap/tests/backend/helpers.ts create mode 100644 plugins/ldap/tests/backend/ldap.test.ts create mode 100644 plugins/ldap/tests/frontend/activate.test.tsx create mode 100644 plugins/ldap/tsconfig.json create mode 100644 plugins/ldap/vitest.config.ts create mode 100644 plugins/sso/locales/en.json create mode 100644 plugins/sso/locales/translated/af_ZA.json create mode 100644 plugins/sso/locales/translated/ar_SA.json create mode 100644 plugins/sso/locales/translated/bg_BG.json create mode 100644 plugins/sso/locales/translated/bn_BD.json create mode 100644 plugins/sso/locales/translated/ca_ES.json create mode 100644 plugins/sso/locales/translated/cs_CZ.json create mode 100644 plugins/sso/locales/translated/da_DK.json create mode 100644 plugins/sso/locales/translated/de_DE.json create mode 100644 plugins/sso/locales/translated/el_GR.json create mode 100644 plugins/sso/locales/translated/es_ES.json create mode 100644 plugins/sso/locales/translated/fi_FI.json create mode 100644 plugins/sso/locales/translated/fr_FR.json create mode 100644 plugins/sso/locales/translated/he_IL.json create mode 100644 plugins/sso/locales/translated/hi_IN.json create mode 100644 plugins/sso/locales/translated/hu_HU.json create mode 100644 plugins/sso/locales/translated/id_ID.json create mode 100644 plugins/sso/locales/translated/it_IT.json create mode 100644 plugins/sso/locales/translated/ja_JP.json create mode 100644 plugins/sso/locales/translated/ko_KR.json create mode 100644 plugins/sso/locales/translated/nl_NL.json create mode 100644 plugins/sso/locales/translated/no_NO.json create mode 100644 plugins/sso/locales/translated/pl_PL.json create mode 100644 plugins/sso/locales/translated/pt_BR.json create mode 100644 plugins/sso/locales/translated/pt_PT.json create mode 100644 plugins/sso/locales/translated/ro_RO.json create mode 100644 plugins/sso/locales/translated/ru_RU.json create mode 100644 plugins/sso/locales/translated/sr_SP.json create mode 100644 plugins/sso/locales/translated/sv_SE.json create mode 100644 plugins/sso/locales/translated/th_TH.json create mode 100644 plugins/sso/locales/translated/tr_TR.json create mode 100644 plugins/sso/locales/translated/uk_UA.json create mode 100644 plugins/sso/locales/translated/vi_VN.json create mode 100644 plugins/sso/locales/translated/zh_CN.json create mode 100644 plugins/sso/locales/translated/zh_TW.json create mode 100644 plugins/sso/manifest.json create mode 100644 plugins/sso/migrations/mysql/0001_adopt_sso_providers.sql create mode 100644 plugins/sso/migrations/mysql/0002_legacy_callback.sql create mode 100644 plugins/sso/migrations/postgres/0001_adopt_sso_providers.sql create mode 100644 plugins/sso/migrations/postgres/0002_legacy_callback.sql create mode 100644 plugins/sso/migrations/snapshot.json create mode 100644 plugins/sso/migrations/sqlite/0001_adopt_sso_providers.sql create mode 100644 plugins/sso/migrations/sqlite/0002_legacy_callback.sql create mode 100644 plugins/sso/package.json create mode 100644 plugins/sso/src/backend/index.ts create mode 100644 plugins/sso/src/backend/login.ts rename src/backend/database/routes/user-oidc-utils.ts => plugins/sso/src/backend/oidc-protocol.ts (64%) create mode 100644 plugins/sso/src/backend/providers.ts create mode 100644 plugins/sso/src/backend/routes.ts create mode 100644 plugins/sso/src/backend/tables.ts create mode 100644 plugins/sso/src/backend/types.ts create mode 100644 plugins/sso/src/frontend/OidcLoginButtons.tsx create mode 100644 plugins/sso/src/frontend/ProvidersSetting.tsx create mode 100644 plugins/sso/src/frontend/index.tsx create mode 100644 plugins/sso/src/frontend/sso-api.ts create mode 100644 plugins/sso/tests/backend/helpers.ts rename src/backend/tests/database/routes/user-oidc-utils.test.ts => plugins/sso/tests/backend/oidc-protocol.test.ts (86%) create mode 100644 plugins/sso/tests/backend/sso.test.ts create mode 100644 plugins/sso/tests/frontend/activate.test.tsx create mode 100644 plugins/sso/tsconfig.json create mode 100644 plugins/sso/vitest.config.ts create mode 100644 src/backend/auth/allowed-users.ts delete mode 100644 src/backend/auth/legacy/ldap-login.ts delete mode 100644 src/backend/auth/legacy/oidc-login.ts delete mode 100644 src/backend/database/repositories/sso-provider-repository.ts create mode 100644 src/backend/database/routes/auth-compat-routes.ts delete mode 100644 src/backend/database/routes/ldap-auth-routes.ts delete mode 100644 src/backend/database/routes/sso-provider-routes.ts create mode 100644 src/backend/tests/auth/allowed-users.test.ts delete mode 100644 src/backend/tests/auth/legacy-methods.test.ts delete mode 100644 src/backend/tests/database/repositories/sso-provider-repository.test.ts create mode 100644 src/backend/tests/database/routes/auth-compat-routes.test.ts delete mode 100644 src/backend/tests/database/routes/sso-provider-routes.test.ts create mode 100644 src/backend/tests/database/routes/user-oidc-account-routes.test.ts create mode 100644 src/backend/tests/plugins/ctx-auth-login.test.ts create mode 100644 src/backend/tests/utils/ldap-provider-migration.test.ts create mode 100644 src/backend/utils/crypto-migration/ldap-provider-migration.ts create mode 100644 src/backend/utils/crypto-migration/plugin-data-moves.ts delete mode 100644 src/ui/api/sso-provider-api.ts delete mode 100644 src/ui/sidebar/SSOProviderDialog.tsx diff --git a/drizzle/mysql/0052_illegal_gladiator.sql b/drizzle/mysql/0052_illegal_gladiator.sql new file mode 100644 index 000000000..9878818dd --- /dev/null +++ b/drizzle/mysql/0052_illegal_gladiator.sql @@ -0,0 +1,4 @@ +-- sso_providers moves to the sso plugin's adopted table at activation, and +-- its LDAP rows are copied into the ldap plugin by a boot migration after +-- this runs. Dropping it here would lose the data first. +SELECT 1; diff --git a/drizzle/mysql/meta/0052_snapshot.json b/drizzle/mysql/meta/0052_snapshot.json new file mode 100644 index 000000000..5aa5e50e3 --- /dev/null +++ b/drizzle/mysql/meta/0052_snapshot.json @@ -0,0 +1,5457 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "ee1565c4-881c-45f8-b084-d32e36893cea", + "prevId": "8475e76e-38d8-47eb-b6a8-c4eb686e0a6f", + "tables": { + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "api_keys_id": { + "name": "api_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audit_logs_id": { + "name": "audit_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_access": { + "name": "credential_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('use')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_access_id": { + "name": "credential_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "credential_sidebar_preferences_user_id": { + "name": "credential_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_dismissed_alerts_user_id": { + "name": "idx_dismissed_alerts_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dismissed_alerts_id": { + "name": "dismissed_alerts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "folder_access": { + "name": "folder_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + "owner_user_id", + "folder" + ], + "isUnique": false + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "folder_access_id": { + "name": "folder_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('connect')" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_access_id": { + "name": "host_access_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "host_sidebar_preferences_user_id": { + "name": "host_sidebar_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('ssh')" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_web_ui": { + "name": "enable_web_ui", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "web_ui_config": { + "name": "web_ui_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "ssh_port": { + "name": "ssh_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('sha256')" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_data_id": { + "name": "ssh_data_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "notification_channels_id": { + "name": "notification_channels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "opkssh_tokens_id": { + "name": "opkssh_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_install_counts": { + "name": "plugin_install_counts", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('aggregate-telemetry')" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + "plugin_id", + "registry_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_install_counts_id": { + "name": "plugin_install_counts_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_migrations": { + "name": "plugin_migrations", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "migration_id": { + "name": "migration_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + "plugin_id", + "migration_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_migrations_id": { + "name": "plugin_migrations_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_permission_grants": { + "name": "plugin_permission_grants", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capability": { + "name": "capability", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('admin')" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + "plugin_id", + "capability" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_permission_grants_id": { + "name": "plugin_permission_grants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_registries": { + "name": "plugin_registries", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('community')" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugin_registries_id": { + "name": "plugin_registries_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_settings": { + "name": "plugin_settings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted": { + "name": "encrypted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + "plugin_id", + "scope", + "scope_id", + "key" + ], + "isUnique": true + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + "plugin_id", + "scope" + ], + "isUnique": false + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_settings_id": { + "name": "plugin_settings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugin_storage": { + "name": "plugin_storage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + "plugin_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "plugin_storage_id": { + "name": "plugin_storage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "plugins": { + "name": "plugins", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('available')" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('community')" + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('disabled')" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + "registry_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "plugins_id": { + "name": "plugins_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "role_name": { + "name": "role_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + "role_name", + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rbac_applied_defaults_id": { + "name": "rbac_applied_defaults_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rbac_known_permissions": { + "name": "rbac_known_permissions", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rbac_known_permissions_id": { + "name": "rbac_known_permissions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "recent_activity_id": { + "name": "recent_activity_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "roles_id": { + "name": "roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ] + } + }, + "checkConstraint": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sessions_id": { + "name": "sessions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "settings_key": { + "name": "settings_key", + "columns": [ + "key" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_credential_secrets": { + "name": "shared_credential_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('password')" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + "credential_access_id", + "target_user_id" + ], + "isUnique": true + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + "target_user_id", + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_credential_secrets_id": { + "name": "shared_credential_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_auth_overrides_id": { + "name": "shared_host_auth_overrides_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('credential')" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "shared_host_secrets_id": { + "name": "shared_host_secrets_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credential_usage_id": { + "name": "ssh_credential_usage_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_credentials_id": { + "name": "ssh_credentials_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ssh_folders_id": { + "name": "ssh_folders_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_tombstones_id": { + "name": "sync_tombstones_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identities_id": { + "name": "termix_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_ca_id": { + "name": "termix_identity_ca_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ] + } + }, + "checkConstraint": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('manual')" + }, + "credential_id": { + "name": "credential_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "termix_identity_keys_id": { + "name": "termix_identity_keys_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "trusted_devices_id": { + "name": "trusted_devices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ui_preferences_user_id": { + "name": "ui_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_external_identities": { + "name": "user_external_identities", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + "provider_id", + "subject" + ], + "isUnique": true + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_external_identities_id": { + "name": "user_external_identities_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_open_tabs_id": { + "name": "user_open_tabs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_defaults": { + "name": "terminal_defaults", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_macros": { + "name": "terminal_macros", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_preferences_user_id": { + "name": "user_preferences_user_id", + "columns": [ + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_roles_id": { + "name": "user_roles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user_second_factors": { + "name": "user_second_factors", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "factor_id": { + "name": "factor_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + "user_id", + "plugin_id", + "factor_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_second_factors_id": { + "name": "user_second_factors_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "('openid email profile')" + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_profiles_id": { + "name": "vault_profiles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ] + } + }, + "checkConstraint": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vault_tokens_id": { + "name": "vault_tokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/mysql/meta/_journal.json b/drizzle/mysql/meta/_journal.json index 1a6acfbe1..6ed16ea1c 100644 --- a/drizzle/mysql/meta/_journal.json +++ b/drizzle/mysql/meta/_journal.json @@ -365,6 +365,13 @@ "when": 1790285910582, "tag": "0051_icy_hercules", "breakpoints": true + }, + { + "idx": 52, + "version": "5", + "when": 1790287990810, + "tag": "0052_illegal_gladiator", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/postgres/0053_keen_thanos.sql b/drizzle/postgres/0053_keen_thanos.sql new file mode 100644 index 000000000..9878818dd --- /dev/null +++ b/drizzle/postgres/0053_keen_thanos.sql @@ -0,0 +1,4 @@ +-- sso_providers moves to the sso plugin's adopted table at activation, and +-- its LDAP rows are copied into the ldap plugin by a boot migration after +-- this runs. Dropping it here would lose the data first. +SELECT 1; diff --git a/drizzle/postgres/meta/0053_snapshot.json b/drizzle/postgres/meta/0053_snapshot.json new file mode 100644 index 000000000..b513e59ee --- /dev/null +++ b/drizzle/postgres/meta/0053_snapshot.json @@ -0,0 +1,5331 @@ +{ + "id": "728c9348-9ca1-47d7-990d-310e56d1c4b7", + "prevId": "57c14e5d-ae42-436c-8317-020b807969e4", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "success": { + "name": "success", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_access": { + "name": "credential_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'use'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dismissed_alerts": { + "name": "dismissed_alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_dismissed_alerts_user_id": { + "name": "idx_dismissed_alerts_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.folder_access": { + "name": "folder_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "folder", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_access": { + "name": "host_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_data": { + "name": "ssh_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_web_ui": { + "name": "enable_web_ui", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "web_ui_config": { + "name": "web_ui_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 22 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + { + "expression": "parent_host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_channels": { + "name": "notification_channels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.opkssh_tokens": { + "name": "opkssh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_install_counts": { + "name": "plugin_install_counts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'aggregate-telemetry'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_migrations": { + "name": "plugin_migrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "migration_id": { + "name": "migration_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "migration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_permission_grants": { + "name": "plugin_permission_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "capability": { + "name": "capability", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "capability", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_registries": { + "name": "plugin_registries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'community'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_settings": { + "name": "plugin_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted": { + "name": "encrypted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_storage": { + "name": "plugin_storage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugins": { + "name": "plugins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'available'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'community'" + }, + "registry_id": { + "name": "registry_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'disabled'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + { + "expression": "registry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "role_name": { + "name": "role_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + { + "expression": "role_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rbac_known_permissions": { + "name": "rbac_known_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + { + "expression": "permission", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recent_activity": { + "name": "recent_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_credential_secrets": { + "name": "shared_credential_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'password'" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + { + "expression": "credential_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shared_host_secrets": { + "name": "shared_host_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + { + "expression": "host_access_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "protocol", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credential_usage": { + "name": "ssh_credential_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_credentials": { + "name": "ssh_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pin": { + "name": "pin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_folders": { + "name": "ssh_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_tombstones": { + "name": "sync_tombstones", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identities": { + "name": "termix_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_ca": { + "name": "termix_identity_ca", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "nullsNotDistinct": false, + "columns": [ + "identity_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.termix_identity_keys": { + "name": "termix_identity_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trusted_devices": { + "name": "trusted_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ui_preferences": { + "name": "ui_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_external_identities": { + "name": "user_external_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_open_tabs": { + "name": "user_open_tabs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_defaults": { + "name": "terminal_defaults", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_macros": { + "name": "terminal_macros", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_second_factors": { + "name": "user_second_factors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "factor_id": { + "name": "factor_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "factor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid email profile'" + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_profiles": { + "name": "vault_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sync_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vault_tokens": { + "name": "vault_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/postgres/meta/_journal.json b/drizzle/postgres/meta/_journal.json index b035e2ac7..12e4d1813 100644 --- a/drizzle/postgres/meta/_journal.json +++ b/drizzle/postgres/meta/_journal.json @@ -372,6 +372,13 @@ "when": 1790285909968, "tag": "0052_motionless_krista_starr", "breakpoints": true + }, + { + "idx": 53, + "version": "7", + "when": 1790287990260, + "tag": "0053_keen_thanos", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/sqlite/0049_glamorous_hemingway.sql b/drizzle/sqlite/0049_glamorous_hemingway.sql new file mode 100644 index 000000000..9878818dd --- /dev/null +++ b/drizzle/sqlite/0049_glamorous_hemingway.sql @@ -0,0 +1,4 @@ +-- sso_providers moves to the sso plugin's adopted table at activation, and +-- its LDAP rows are copied into the ldap plugin by a boot migration after +-- this runs. Dropping it here would lose the data first. +SELECT 1; diff --git a/drizzle/sqlite/meta/0049_snapshot.json b/drizzle/sqlite/meta/0049_snapshot.json new file mode 100644 index 000000000..adb18ac74 --- /dev/null +++ b/drizzle/sqlite/meta/0049_snapshot.json @@ -0,0 +1,5154 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "52c3f87e-a998-45b1-abf6-89c819508144", + "prevId": "cda293ef-98c8-4572-98c7-70c8bd244c40", + "tables": { + "api_keys": { + "name": "api_keys", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "idx_api_keys_user_id": { + "name": "idx_api_keys_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_audit_logs_timestamp": { + "name": "idx_audit_logs_timestamp", + "columns": [ + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_user_ts": { + "name": "idx_audit_logs_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_action_ts": { + "name": "idx_audit_logs_action_ts", + "columns": [ + "action", + "timestamp" + ], + "isUnique": false + }, + "idx_audit_logs_resource_ts": { + "name": "idx_audit_logs_resource_ts", + "columns": [ + "resource_type", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_access": { + "name": "credential_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'use'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_credential_access_user_id": { + "name": "idx_credential_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_credential_access_role_id": { + "name": "idx_credential_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_credential_access_credential_id": { + "name": "idx_credential_access_credential_id", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "credential_access_credential_id_ssh_credentials_id_fk": { + "name": "credential_access_credential_id_ssh_credentials_id_fk", + "tableFrom": "credential_access", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_user_id_users_id_fk": { + "name": "credential_access_user_id_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_role_id_roles_id_fk": { + "name": "credential_access_role_id_roles_id_fk", + "tableFrom": "credential_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_access_granted_by_users_id_fk": { + "name": "credential_access_granted_by_users_id_fk", + "tableFrom": "credential_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "credential_sidebar_preferences": { + "name": "credential_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "credential_sidebar_preferences_user_id_users_id_fk": { + "name": "credential_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "credential_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dismissed_alerts": { + "name": "dismissed_alerts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alert_id": { + "name": "alert_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_dismissed_alerts_user_id": { + "name": "idx_dismissed_alerts_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "dismissed_alerts_user_id_users_id_fk": { + "name": "dismissed_alerts_user_id_users_id_fk", + "tableFrom": "dismissed_alerts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "folder_access": { + "name": "folder_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_folder_access_owner_folder": { + "name": "idx_folder_access_owner_folder", + "columns": [ + "owner_user_id", + "folder" + ], + "isUnique": false + } + }, + "foreignKeys": { + "folder_access_owner_user_id_users_id_fk": { + "name": "folder_access_owner_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_user_id_users_id_fk": { + "name": "folder_access_user_id_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_role_id_roles_id_fk": { + "name": "folder_access_role_id_roles_id_fk", + "tableFrom": "folder_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_access_granted_by_users_id_fk": { + "name": "folder_access_granted_by_users_id_fk", + "tableFrom": "folder_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_access": { + "name": "host_access", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_level": { + "name": "permission_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'connect'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_host_access_user_id": { + "name": "idx_host_access_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_host_access_role_id": { + "name": "idx_host_access_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + }, + "idx_host_access_host_id": { + "name": "idx_host_access_host_id", + "columns": [ + "host_id" + ], + "isUnique": false + }, + "idx_host_access_expires_at": { + "name": "idx_host_access_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_access_host_id_ssh_data_id_fk": { + "name": "host_access_host_id_ssh_data_id_fk", + "tableFrom": "host_access", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_user_id_users_id_fk": { + "name": "host_access_user_id_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_role_id_roles_id_fk": { + "name": "host_access_role_id_roles_id_fk", + "tableFrom": "host_access", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "host_access_granted_by_users_id_fk": { + "name": "host_access_granted_by_users_id_fk", + "tableFrom": "host_access", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_sidebar_preferences": { + "name": "host_sidebar_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "host_sidebar_preferences_user_id_users_id_fk": { + "name": "host_sidebar_preferences_user_id_users_id_fk", + "tableFrom": "host_sidebar_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_data": { + "name": "ssh_data", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_host_id": { + "name": "parent_host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "use_warpgate": { + "name": "use_warpgate", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "share_ssh_auth": { + "name": "share_ssh_auth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "force_keyboard_interactive": { + "name": "force_keyboard_interactive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sudo_password": { + "name": "sudo_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_password": { + "name": "autostart_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key": { + "name": "autostart_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autostart_key_password": { + "name": "autostart_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "override_credential_username": { + "name": "override_credential_username", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_profile_id": { + "name": "vault_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_terminal": { + "name": "enable_terminal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_session_logging": { + "name": "enable_session_logging", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "allow_session_sharing": { + "name": "allow_session_sharing", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_command_history": { + "name": "enable_command_history", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enable_tunnel": { + "name": "enable_tunnel", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "tunnel_connections": { + "name": "tunnel_connections", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "jump_hosts": { + "name": "jump_hosts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_file_manager": { + "name": "enable_file_manager", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "scp_legacy": { + "name": "scp_legacy", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_web_ui": { + "name": "enable_web_ui", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_tmux_monitor": { + "name": "enable_tmux_monitor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enable_terminal_toolbar": { + "name": "enable_terminal_toolbar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_terminal_in_sidebar": { + "name": "show_terminal_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "show_file_manager_in_sidebar": { + "name": "show_file_manager_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_tunnel_in_sidebar": { + "name": "show_tunnel_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_docker_in_sidebar": { + "name": "show_docker_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_server_stats_in_sidebar": { + "name": "show_server_stats_in_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_path": { + "name": "default_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_check_enabled": { + "name": "status_check_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "status_check_interval": { + "name": "status_check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "web_ui_config": { + "name": "web_ui_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_config": { + "name": "terminal_config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quick_actions": { + "name": "quick_actions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enable_ssh": { + "name": "enable_ssh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "ssh_port": { + "name": "ssh_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "rdp_credential_id": { + "name": "rdp_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_user": { + "name": "rdp_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_password": { + "name": "rdp_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_domain": { + "name": "rdp_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_credential_id": { + "name": "vnc_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_password": { + "name": "vnc_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_user": { + "name": "vnc_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_user": { + "name": "telnet_user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_password": { + "name": "telnet_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_credential_id": { + "name": "telnet_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rdp_auth_type": { + "name": "rdp_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vnc_auth_type": { + "name": "vnc_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telnet_auth_type": { + "name": "telnet_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "use_socks5": { + "name": "use_socks5", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_host": { + "name": "socks5_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_port": { + "name": "socks5_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_username": { + "name": "socks5_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_password": { + "name": "socks5_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "socks5_proxy_chain": { + "name": "socks5_proxy_chain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_origin": { + "name": "connection_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_knock_sequence": { + "name": "port_knock_sequence", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_fingerprint": { + "name": "host_key_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_type": { + "name": "host_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_algorithm": { + "name": "host_key_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'sha256'" + }, + "host_key_first_seen": { + "name": "host_key_first_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_last_verified": { + "name": "host_key_last_verified", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_key_changed_count": { + "name": "host_key_changed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_data_sync_id_unique": { + "name": "ssh_data_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_data_user_id": { + "name": "idx_ssh_data_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_ssh_data_parent_host": { + "name": "idx_ssh_data_parent_host", + "columns": [ + "parent_host_id" + ], + "isUnique": false + }, + "idx_ssh_data_credential": { + "name": "idx_ssh_data_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_data_user_id_users_id_fk": { + "name": "ssh_data_user_id_users_id_fk", + "tableFrom": "ssh_data", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_data_parent_host_id_ssh_data_id_fk": { + "name": "ssh_data_parent_host_id_ssh_data_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_data", + "columnsFrom": [ + "parent_host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vault_profile_id_vault_profiles_id_fk": { + "name": "ssh_data_vault_profile_id_vault_profiles_id_fk", + "tableFrom": "ssh_data", + "tableTo": "vault_profiles", + "columnsFrom": [ + "vault_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_rdp_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_rdp_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "rdp_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_vnc_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_vnc_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "vnc_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ssh_data_telnet_credential_id_ssh_credentials_id_fk": { + "name": "ssh_data_telnet_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_data", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "telnet_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_user_id_users_id_fk": { + "name": "notification_channels_user_id_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "opkssh_tokens": { + "name": "opkssh_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sub": { + "name": "sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_opkssh_tokens_user_host": { + "name": "idx_opkssh_tokens_user_host", + "columns": [ + "user_id", + "host_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "opkssh_tokens_user_id_users_id_fk": { + "name": "opkssh_tokens_user_id_users_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "opkssh_tokens_host_id_ssh_data_id_fk": { + "name": "opkssh_tokens_host_id_ssh_data_id_fk", + "tableFrom": "opkssh_tokens", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_install_counts": { + "name": "plugin_install_counts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "registry_id": { + "name": "registry_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'aggregate-telemetry'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_install_counts_plugin_registry": { + "name": "idx_plugin_install_counts_plugin_registry", + "columns": [ + "plugin_id", + "registry_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_migrations": { + "name": "plugin_migrations", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "migration_id": { + "name": "migration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_migrations_plugin_migration": { + "name": "idx_plugin_migrations_plugin_migration", + "columns": [ + "plugin_id", + "migration_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_permission_grants": { + "name": "plugin_permission_grants", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capability": { + "name": "capability", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'admin'" + } + }, + "indexes": { + "idx_plugin_permission_grants_plugin_capability": { + "name": "idx_plugin_permission_grants_plugin_capability", + "columns": [ + "plugin_id", + "capability" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_permission_grants_plugin_id_plugins_id_fk": { + "name": "plugin_permission_grants_plugin_id_plugins_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_permission_grants_granted_by_users_id_fk": { + "name": "plugin_permission_grants_granted_by_users_id_fk", + "tableFrom": "plugin_permission_grants", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_registries": { + "name": "plugin_registries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'community'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "signing_key": { + "name": "signing_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_index_hash": { + "name": "last_index_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_settings": { + "name": "plugin_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted": { + "name": "encrypted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_settings_scope_key": { + "name": "idx_plugin_settings_scope_key", + "columns": [ + "plugin_id", + "scope", + "scope_id", + "key" + ], + "isUnique": true + }, + "idx_plugin_settings_plugin_scope": { + "name": "idx_plugin_settings_plugin_scope", + "columns": [ + "plugin_id", + "scope" + ], + "isUnique": false + } + }, + "foreignKeys": { + "plugin_settings_plugin_id_plugins_id_fk": { + "name": "plugin_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_storage": { + "name": "plugin_storage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_plugin_storage_plugin_key": { + "name": "idx_plugin_storage_plugin_key", + "columns": [ + "plugin_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "plugin_storage_plugin_id_plugins_id_fk": { + "name": "plugin_storage_plugin_id_plugins_id_fk", + "tableFrom": "plugin_storage", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugins": { + "name": "plugins", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'available'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'community'" + }, + "registry_id": { + "name": "registry_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'disabled'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "installed_at": { + "name": "installed_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "auto_update": { + "name": "auto_update", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "manifest_json": { + "name": "manifest_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_plugins_registry_id": { + "name": "idx_plugins_registry_id", + "columns": [ + "registry_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rbac_applied_defaults": { + "name": "rbac_applied_defaults", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "role_name": { + "name": "role_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_at": { + "name": "applied_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_applied_defaults_role_permission": { + "name": "idx_rbac_applied_defaults_role_permission", + "columns": [ + "role_name", + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rbac_known_permissions": { + "name": "rbac_known_permissions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_rbac_known_permissions_permission": { + "name": "idx_rbac_known_permissions_permission", + "columns": [ + "permission" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "recent_activity": { + "name": "recent_activity", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_name": { + "name": "host_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_recent_activity_user_ts": { + "name": "idx_recent_activity_user_ts", + "columns": [ + "user_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "recent_activity_user_id_users_id_fk": { + "name": "recent_activity_user_id_users_id_fk", + "tableFrom": "recent_activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recent_activity_host_id_ssh_data_id_fk": { + "name": "recent_activity_host_id_ssh_data_id_fk", + "tableFrom": "recent_activity", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "jwt_token": { + "name": "jwt_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "oidc_sub": { + "name": "oidc_sub", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_sid": { + "name": "oidc_sid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_sessions_user_id": { + "name": "idx_sessions_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_sessions_expires_at": { + "name": "idx_sessions_expires_at", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_credential_secrets": { + "name": "shared_credential_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_access_id": { + "name": "credential_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'password'" + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_credential_secrets_scope": { + "name": "idx_shared_credential_secrets_scope", + "columns": [ + "credential_access_id", + "target_user_id" + ], + "isUnique": true + }, + "idx_shared_credential_secrets_target": { + "name": "idx_shared_credential_secrets_target", + "columns": [ + "target_user_id", + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "shared_credential_secrets_target_user_id_users_id_fk": { + "name": "shared_credential_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_credential_secrets_credential_id_ssh_credentials_id_fk": { + "name": "shared_credential_secrets_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_cred_secrets_access_id_fk": { + "name": "shared_cred_secrets_access_id_fk", + "tableFrom": "shared_credential_secrets", + "tableTo": "credential_access", + "columnsFrom": [ + "credential_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_auth_overrides": { + "name": "shared_host_auth_overrides", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "shared_host_auth_overrides_host_user_protocol_unique": { + "name": "shared_host_auth_overrides_host_user_protocol_unique", + "columns": [ + "host_id", + "user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_auth_overrides_host_id_ssh_data_id_fk": { + "name": "shared_host_auth_overrides_host_id_ssh_data_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_user_id_users_id_fk": { + "name": "shared_host_auth_overrides_user_id_users_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_auth_overrides_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_auth_overrides", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shared_host_secrets": { + "name": "shared_host_secrets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "host_access_id": { + "name": "host_access_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ssh'" + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credential'" + }, + "original_credential_id": { + "name": "original_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_username": { + "name": "encrypted_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_auth_type": { + "name": "encrypted_auth_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_password": { + "name": "encrypted_key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_key_type": { + "name": "encrypted_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "encrypted_domain": { + "name": "encrypted_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_shared_host_secrets_scope": { + "name": "idx_shared_host_secrets_scope", + "columns": [ + "host_access_id", + "target_user_id", + "protocol" + ], + "isUnique": true + } + }, + "foreignKeys": { + "shared_host_secrets_host_access_id_host_access_id_fk": { + "name": "shared_host_secrets_host_access_id_host_access_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "host_access", + "columnsFrom": [ + "host_access_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_target_user_id_users_id_fk": { + "name": "shared_host_secrets_target_user_id_users_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "users", + "columnsFrom": [ + "target_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shared_host_secrets_original_credential_id_ssh_credentials_id_fk": { + "name": "shared_host_secrets_original_credential_id_ssh_credentials_id_fk", + "tableFrom": "shared_host_secrets", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "original_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credential_usage": { + "name": "ssh_credential_usage", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_ssh_credential_usage_credential": { + "name": "idx_ssh_credential_usage_credential", + "columns": [ + "credential_id" + ], + "isUnique": false + }, + "idx_ssh_credential_usage_user": { + "name": "idx_ssh_credential_usage_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credential_usage_credential_id_ssh_credentials_id_fk": { + "name": "ssh_credential_usage_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_host_id_ssh_data_id_fk": { + "name": "ssh_credential_usage_host_id_ssh_data_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_credential_usage_user_id_users_id_fk": { + "name": "ssh_credential_usage_user_id_users_id_fk", + "tableFrom": "ssh_credential_usage", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_credentials": { + "name": "ssh_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin": { + "name": "pin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(16384)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_password": { + "name": "key_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detected_key_type": { + "name": "detected_key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cert_public_key": { + "name": "cert_public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_credentials_sync_id_unique": { + "name": "ssh_credentials_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_credentials_user_id": { + "name": "idx_ssh_credentials_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_credentials_user_id_users_id_fk": { + "name": "ssh_credentials_user_id_users_id_fk", + "tableFrom": "ssh_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ssh_folders": { + "name": "ssh_folders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "ssh_folders_sync_id_unique": { + "name": "ssh_folders_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + }, + "idx_ssh_folders_user_id": { + "name": "idx_ssh_folders_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ssh_folders_user_id_users_id_fk": { + "name": "ssh_folders_user_id_users_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ssh_folders_credential_id_ssh_credentials_id_fk": { + "name": "ssh_folders_credential_id_ssh_credentials_id_fk", + "tableFrom": "ssh_folders", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sync_tombstones": { + "name": "sync_tombstones", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "sync_tombstones_user_id_users_id_fk": { + "name": "sync_tombstones_user_id_users_id_fk", + "tableFrom": "sync_tombstones", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identities": { + "name": "termix_identities", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identities_user_id_unique": { + "name": "termix_identities_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + }, + "termix_identities_handle_unique": { + "name": "termix_identities_handle_unique", + "columns": [ + "handle" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identities_user_id_users_id_fk": { + "name": "termix_identities_user_id_users_id_fk", + "tableFrom": "termix_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_ca": { + "name": "termix_identity_ca", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(4096)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validity_days": { + "name": "validity_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "termix_identity_ca_identity_id_unique": { + "name": "termix_identity_ca_identity_id_unique", + "columns": [ + "identity_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "termix_identity_ca_identity_id_termix_identities_id_fk": { + "name": "termix_identity_ca_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_ca_user_id_users_id_fk": { + "name": "termix_identity_ca_user_id_users_id_fk", + "tableFrom": "termix_identity_ca", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "termix_identity_keys": { + "name": "termix_identity_keys", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "identity_id": { + "name": "identity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "algorithm": { + "name": "algorithm", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "termix_identity_keys_identity_id_termix_identities_id_fk": { + "name": "termix_identity_keys_identity_id_termix_identities_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "termix_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_user_id_users_id_fk": { + "name": "termix_identity_keys_user_id_users_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "termix_identity_keys_credential_id_ssh_credentials_id_fk": { + "name": "termix_identity_keys_credential_id_ssh_credentials_id_fk", + "tableFrom": "termix_identity_keys", + "tableTo": "ssh_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "trusted_devices": { + "name": "trusted_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_fingerprint": { + "name": "device_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_info": { + "name": "device_info", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_trusted_devices_user_id": { + "name": "idx_trusted_devices_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "trusted_devices_user_id_users_id_fk": { + "name": "trusted_devices_user_id_users_id_fk", + "tableFrom": "trusted_devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ui_preferences": { + "name": "ui_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "ui_preferences_user_id_users_id_fk": { + "name": "ui_preferences_user_id_users_id_fk", + "tableFrom": "ui_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_external_identities": { + "name": "user_external_identities", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_external_identities_provider_subject": { + "name": "idx_user_external_identities_provider_subject", + "columns": [ + "provider_id", + "subject" + ], + "isUnique": true + }, + "idx_user_external_identities_user": { + "name": "idx_user_external_identities_user", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_external_identities_user_id_users_id_fk": { + "name": "user_external_identities_user_id_users_id_fk", + "tableFrom": "user_external_identities", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_open_tabs": { + "name": "user_open_tabs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_type": { + "name": "tab_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host_id": { + "name": "host_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "backend_session_id": { + "name": "backend_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_open_tabs_user_id": { + "name": "idx_user_open_tabs_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_open_tabs_user_id_users_id_fk": { + "name": "user_open_tabs_user_id_users_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_open_tabs_host_id_ssh_data_id_fk": { + "name": "user_open_tabs_host_id_ssh_data_id_fk", + "tableFrom": "user_open_tabs", + "tableTo": "ssh_data", + "columnsFrom": [ + "host_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "reopen_tabs_on_login": { + "name": "reopen_tabs_on_login", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "font_size": { + "name": "font_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "storage_mode": { + "name": "storage_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_autocomplete": { + "name": "command_autocomplete", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "command_palette_enabled": { + "name": "command_palette_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_host_tags": { + "name": "show_host_tags", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "host_tray_on_click": { + "name": "host_tray_on_click", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pin_app_rail": { + "name": "pin_app_rail", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expand_app_rail_on_hover": { + "name": "expand_app_rail_on_hover", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_pin_app_rail_button": { + "name": "show_pin_app_rail_button", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folders_collapsed": { + "name": "folders_collapsed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_snippet_execution": { + "name": "confirm_snippet_execution", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disable_update_check": { + "name": "disable_update_check", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_tab_close": { + "name": "confirm_tab_close", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_rail_tabs": { + "name": "hidden_rail_tabs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compact_host_view": { + "name": "compact_host_view", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_color_scheme": { + "name": "status_color_scheme", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_themes": { + "name": "custom_themes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "custom_keybindings": { + "name": "custom_keybindings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_defaults": { + "name": "terminal_defaults", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_macros": { + "name": "terminal_macros", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "granted_at": { + "name": "granted_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_roles_user_role": { + "name": "idx_user_roles_user_role", + "columns": [ + "user_id", + "role_id" + ], + "isUnique": true + }, + "idx_user_roles_role_id": { + "name": "idx_user_roles_role_id", + "columns": [ + "role_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_granted_by_users_id_fk": { + "name": "user_roles_granted_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "granted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_second_factors": { + "name": "user_second_factors", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "factor_id": { + "name": "factor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "idx_user_second_factors_user_factor": { + "name": "idx_user_second_factors_user_factor", + "columns": [ + "user_id", + "plugin_id", + "factor_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_second_factors_user_id_users_id_fk": { + "name": "user_second_factors_user_id_users_id_fk", + "tableFrom": "user_second_factors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_oidc": { + "name": "is_oidc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "oidc_identifier": { + "name": "oidc_identifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identifier_path": { + "name": "identifier_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name_path": { + "name": "name_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'openid email profile'" + }, + "registered_at": { + "name": "registered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "donation_modal_dismissed": { + "name": "donation_modal_dismissed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_profiles": { + "name": "vault_profiles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "folder": { + "name": "folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vault_addr": { + "name": "vault_addr", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vault_namespace": { + "name": "vault_namespace", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_mount": { + "name": "oidc_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oidc_role": { + "name": "oidc_role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_mount": { + "name": "ssh_mount", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_role": { + "name": "ssh_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valid_principals": { + "name": "valid_principals", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key_type": { + "name": "key_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "shared": { + "name": "shared", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sync_id": { + "name": "sync_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "vault_profiles_sync_id_unique": { + "name": "vault_profiles_sync_id_unique", + "columns": [ + "sync_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_profiles_user_id_users_id_fk": { + "name": "vault_profiles_user_id_users_id_fk", + "tableFrom": "vault_profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vault_tokens": { + "name": "vault_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile_id": { + "name": "profile_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ssh_cert": { + "name": "ssh_cert", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "private_key": { + "name": "private_key", + "type": "text(8192)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_vault_tokens_user_profile": { + "name": "idx_vault_tokens_user_profile", + "columns": [ + "user_id", + "profile_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "vault_tokens_user_id_users_id_fk": { + "name": "vault_tokens_user_id_users_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vault_tokens_profile_id_vault_profiles_id_fk": { + "name": "vault_tokens_profile_id_vault_profiles_id_fk", + "tableFrom": "vault_tokens", + "tableTo": "vault_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/sqlite/meta/_journal.json b/drizzle/sqlite/meta/_journal.json index 652c01341..e3e9ca81a 100644 --- a/drizzle/sqlite/meta/_journal.json +++ b/drizzle/sqlite/meta/_journal.json @@ -344,6 +344,13 @@ "when": 1790285909308, "tag": "0048_cold_miek", "breakpoints": true + }, + { + "idx": 49, + "version": "6", + "when": 1790287989611, + "tag": "0049_glamorous_hemingway", + "breakpoints": true } ] } \ No newline at end of file diff --git a/knip.json b/knip.json index b0f3b4798..1709411b8 100644 --- a/knip.json +++ b/knip.json @@ -63,7 +63,6 @@ "express", "guacamole-lite", "guacamole-common-js", - "jose", "js-yaml", "jsonwebtoken", "jszip", diff --git a/package-lock.json b/package-lock.json index adfa7351c..3670bcbca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,6 @@ "@anthropic-ai/sdk": "^0.120.0", "@tanstack/react-virtual": "^3.14.13", "@types/compression": "^1.8.1", - "@types/ldapjs": "^3.0.6", "@types/semver": "^7.8.0", "axios": "^1.19.0", "bcryptjs": "^3.0.3", @@ -30,11 +29,9 @@ "drizzle-orm": "^0.45.2", "express": "^5.2.1", "guacamole-lite": "^1.2.0", - "jose": "^6.2.12", "js-yaml": "^5.2.3", "jsonwebtoken": "^9.0.3", "jszip": "^3.10.2", - "ldapjs": "^3.0.7", "motion": "^13.1.1", "multer": "^2.3.0", "mysql2": "^3.23.4", @@ -6998,6 +6995,10 @@ "resolved": "plugins/host-metrics", "link": true }, + "node_modules/@termix-plugin/ldap": { + "resolved": "plugins/ldap", + "link": true + }, "node_modules/@termix-plugin/network-topology": { "resolved": "plugins/network-topology", "link": true @@ -7034,6 +7035,10 @@ "resolved": "plugins/ssh-terminal", "link": true }, + "node_modules/@termix-plugin/sso": { + "resolved": "plugins/sso", + "link": true + }, "node_modules/@termix-plugin/tailscale": { "resolved": "plugins/tailscale", "link": true @@ -7381,6 +7386,7 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-3.0.6.tgz", "integrity": "sha512-E2Tn1ltJDYBsidOT9QG4engaQeQzRQ9aYNxVmjCkD33F7cIeLPgrRDXAYs0O35mK2YDU20c/+ZkNjeAPRGLM0Q==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -18604,6 +18610,17 @@ "@termix/plugin-sdk": "*" } }, + "plugins/ldap": { + "name": "@termix-plugin/ldap", + "version": "1.0.0", + "dependencies": { + "ldapjs": "^3.0.7" + }, + "devDependencies": { + "@termix/plugin-sdk": "*", + "@types/ldapjs": "^3.0.6" + } + }, "plugins/network-topology": { "name": "@termix-plugin/network-topology", "version": "1.0.0", @@ -18680,6 +18697,17 @@ "@termix/plugin-sdk": "*" } }, + "plugins/sso": { + "name": "@termix-plugin/sso", + "version": "1.0.0", + "dependencies": { + "jose": "^6.2.12", + "undici": "^8.10.2" + }, + "devDependencies": { + "@termix/plugin-sdk": "*" + } + }, "plugins/tailscale": { "name": "@termix-plugin/tailscale", "version": "1.0.0", diff --git a/package.json b/package.json index ca782a4b2..3f4cab937 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,6 @@ "@anthropic-ai/sdk": "^0.120.0", "@tanstack/react-virtual": "^3.14.13", "@types/compression": "^1.8.1", - "@types/ldapjs": "^3.0.6", "@types/semver": "^7.8.0", "axios": "^1.19.0", "bcryptjs": "^3.0.3", @@ -72,11 +71,9 @@ "drizzle-orm": "^0.45.2", "express": "^5.2.1", "guacamole-lite": "^1.2.0", - "jose": "^6.2.12", "js-yaml": "^5.2.3", "jsonwebtoken": "^9.0.3", "jszip": "^3.10.2", - "ldapjs": "^3.0.7", "motion": "^13.1.1", "multer": "^2.3.0", "mysql2": "^3.23.4", diff --git a/packages/plugin-sdk/ARCHITECTURE.md b/packages/plugin-sdk/ARCHITECTURE.md index ed138eed5..0e03c7669 100644 --- a/packages/plugin-sdk/ARCHITECTURE.md +++ b/packages/plugin-sdk/ARCHITECTURE.md @@ -999,11 +999,11 @@ auto-session and trusted devices, plus the base SSH auth types password, key, stored credential, agent and none. Every other login method, second factor and SSH auth method is a plugin through `ctx.auth`. -In 2.9.0, OIDC (and GitHub, Google), LDAP, OPKSSH, Step-CA, Vault, Tailscale -and Warpgate still live in core. They register through the same interfaces -from `src/backend/auth/legacy-providers.ts` and -`src/ui/auth/legacy-auth-ui.tsx` with `pluginId: "core"`. Phase C moves each -one into its plugin by moving its block out of those two files; D1 deletes them. +In 2.9.0, OPKSSH, Step-CA, Vault, Tailscale and Warpgate still live in core. +They register through the same interfaces from +`src/backend/auth/legacy-providers.ts` and `src/ui/auth/legacy-auth-ui.tsx` +with `pluginId: "core"`. Phase C moves each one into its plugin by moving its +block out of those two files; D1 deletes them. Nothing else in core branches on those type names. **C1** moved TOTP and passkeys out: @@ -1043,6 +1043,52 @@ Nothing else in core branches on those type names. `AuthManager.unlockWithSystemKey` (was `authenticateWebAuthnUser`) opens the data key for any identity that carries no password. +**C2** moved external logins out: + +- `sso` registers the `oidc` login method: OIDC, GitHub and Google, one + button per enabled provider (the method's instances), plus the provider + configured through the `OIDC_*` environment variables as instance `0`. It + adopts `sso_providers` as `p_sso_providers`, ignoring any `ldap` rows left + in it. Its public routes are `/plugin-api/sso/start` (sends the browser to + the provider), `/callback` (GET and POST), `/backchannel-logout` and + `/config`; provider management is `/plugin-api/sso/providers`, gated on + `sso.manage`, and renders as the custom `providers` field on the plugin's + admin settings page. Login state (nonce, PKCE verifier, return address) + lives in `ctx.kv` for ten minutes. +- The redirect URI is `/plugin-api/sso/callback`. Providers that + existed before 2.9 keep sending `/users/oidc/callback`, the URI their + identity provider was registered with, because the sso plugin's second + migration sets `legacy_callback` on every adopted row; the env provider + always does. Core answers that old URL with a 308 to the plugin's callback, + and the token exchange sends the same redirect URI the authorize request + did, so strict providers accept it. The admin page shows each provider's + URI and switches it to the new one once the admin has registered that. + `/users/oidc/backchannel-logout` is a 308 to the plugin as well. +- `ldap` registers the `ldap` form method, one form per directory, in its own + `p_ldap_providers`. `utils/crypto-migration/ldap-provider-migration.ts` + copies the 2.8 LDAP rows out of `sso_providers` (adopted or not) with their + ids and deletes them there. It runs from the loader whenever a plugin's + migrations applied something (`plugin-data-moves.ts`), so the rows are in + place before the ldap plugin first activates, at boot or when it is enabled + later. LDAP identities are stored under provider `ldap:` so their ids + can never clash with SSO ones; `external-identity-migration.ts` writes them + that way and the LDAP migration renames any bare ones. +- Both return 2.8's `users.oidc_identifier` value in `legacy`, so pre-2.9 + accounts are found and new ones keep the column filled. The sso plugin + passes its group role map as `roles` and the id token's `sub`/`sid` as + `logoutClaims`; `/backchannel-logout` ends those sessions with + `ctx.auth.revokeSessions`. LDAP counts failed binds with + `ctx.auth.loginRateLimit`, the limiter password login uses. +- Core keeps provisioning (the allowed-users matcher is + `auth/allowed-users.ts`), `oidc_auto_provision`, silent sign-in (it starts + the first redirect method), account linking and the trusted proxy rule: + every `external` method is refused while trusted proxy login is on, and + `/users/proxy-login` refuses while any external method lists an instance. + The 2.8 client routes `/users/oidc/authorize`, `/users/oidc-config` (307 to + `/plugin-api/sso/config`), `/users/sso-providers` and `/users/ldap/login` + live in `database/routes/auth-compat-routes.ts` and only forward to the + registered methods, so with the plugins off they answer 404. + #### One SSH connect pipeline Every SSH connection, core's and plugins', goes through @@ -1171,10 +1217,25 @@ or an external identity. Core then: Routes, all under `/users/auth`: `GET methods` (public), `GET :methodId/start`, `GET|POST :methodId/callback`, `POST :methodId/verify`, `POST second-factor/:factorId/challenge`, `POST second-factor/:factorId/verify`. -A redirect method's provider must send the browser back to -`/users/auth//callback`. The older routes (`/users/login`, +A redirect method's provider sends the browser back to +`/users/auth//callback`, or to a public route of the plugin's own +that calls `ctx.auth.completeRedirectLogin(methodId, req, res)`, which runs +the same callback and pipeline (**C2**, for a URL that has to stay stable in +the provider's configuration). The older routes (`/users/login`, `/users/oidc/*`, `/users/ldap/login`) are thin wrappers over the same -pipeline. `/users/totp/verify-login` stays for 2.8 clients: it answers with +pipeline. + +**Identity extras (C2).** An external identity may also carry `roles` +(`{ desired, managed }`: core adds the desired roles and removes managed ones +the user no longer maps to), `logoutClaims` (`providerId`, `sub`, `sid`, +stored on the session for `ctx.auth.revokeSessions`), `legacy` (the 2.8 +`oidc_identifier` and `sso_provider_id`, only for a plugin that took over a +2.8 method) and `rateLimitKey` (cleared on success; keys are kept apart per +plugin). Core copies only these documented fields, so a plugin cannot set +core-only ones. `ctx.auth.countLinkedUsers(provider)` answers how many users +sign in through a provider, for refusing to delete one still in use. +`ctx.http.baseUrl(req)` is the install's public URL (origin plus base path, +honouring `OIDC_FORCE_HTTPS`) for building callback URLs. `/users/totp/verify-login` stays for 2.8 clients: it answers with the factor named in `factor`, or the user's first required factor, and core never names one itself. @@ -1562,6 +1623,7 @@ Built per plugin in `src/backend/plugins/ctx.ts` and passed to `activate`. | `ctx.db.persist` / `.dialect` | `db:own` (persist only) | **A9**, lazy **B16** | | `ctx.sync.registerEntity` | none | **A3** | | `ctx.http.router` / `ctx.ws.route` / `.upgrade` | `network:serve` | **A4** | +| `ctx.http.baseUrl(req)` | none | **C2** | | `ctx.rbac.has` / `.hasFor` / `.require` | own permissions only | **A5** | | `ctx.capabilities.has` / `.require` | the capability itself | **B11** | | `ctx.hosts.*` | `hosts:read` / `hosts:write` | **B4**, **B5**, **B18** | @@ -1570,6 +1632,7 @@ Built per plugin in `src/backend/plugins/ctx.ts` and passed to `activate`. | `ctx.settings.*` | `settings:read-core` (readCore only) | **A6** | | `ctx.notify.channels` / `.send` | `notify:send` | **B17** | | `ctx.auth.*` | `auth:provide` | **A8** | +| `ctx.auth.completeRedirectLogin` and the rest | `auth:provide` | **C2** | | `ctx.desktop.openIsolatedWindow` | `desktop:window` | **B8** | | `ctx.desktop.launchNativeRdp` / `.available` | `desktop:window` (launch only) | **B14** | | `ctx.credentials.resolveHostProtocol` | `credentials:read` | **B14** | @@ -2361,8 +2424,8 @@ Then, with the app running: The bundled plugins predate the SDK, apart from workspaces (A9), snippets (B2), remote-desktop (B14), docker (B15), host-metrics (B16), automations -(B17), ai (B18), homepage (B19), totp and webauthn (C1), which import -nothing from core. The others still reach core by relative +(B17), ai (B18), homepage (B19), totp and webauthn (C1), sso and ldap (C2), +which import nothing from core. The others still reach core by relative path (`../../../../src/backend/...`), which an esbuild plugin, `packages/plugin-sdk/cli/lib/legacy-core-imports.mjs`, keeps out of the bundle and rewrites to the compiled output path (`../../../backend/backend/...`, or diff --git a/packages/plugin-sdk/FINISH-LIST.md b/packages/plugin-sdk/FINISH-LIST.md index ebfaf6d18..cce02c2d5 100644 --- a/packages/plugin-sdk/FINISH-LIST.md +++ b/packages/plugin-sdk/FINISH-LIST.md @@ -465,6 +465,27 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite and `totp_backup_codes` stay in the database (dropped from `schema.ts`, the drizzle drop is a no-op) until 3.0.0 removes them, after the boot migration has run everywhere. Owner: 3.0.0. +- **C2 (sso/ldap):** `users.is_oidc`, `oidc_identifier`, `sso_provider_id` + and the 2.8 per-user OIDC columns (`client_id`, `issuer_url`, ...), and + `sessions.sso_provider_id`, `oidc_sub`, `oidc_sid`, stay in core. Core + still reads `is_oidc`/`oidc_identifier` for account linking, `/users/me` and + the `$oidc.preferred_username` placeholder, and still writes them from an + identity's `legacy` field so a downgrade works. Rename or drop them once + nothing needs the 2.8 shape. Owner: 3.0.0. +- **C2 (sso/ldap):** `database/routes/auth-compat-routes.ts` keeps the 2.8 + URLs (`/users/oidc/*`, `/users/oidc-config`, `/users/sso-providers`, + `/users/ldap/login`) and so names the `oidc`/`ldap` method ids and the sso + plugin's `/plugin-api/sso/` paths. Keep until identity providers have moved + to the new redirect URI and Termix-Mobile uses `/users/auth/*`. Owner: + 3.0.0. +- **C2 (sso):** the `oidc_auto_provision` and `oidc_silent_login_default` + settings, their routes and admin toggles are generic (every external + method, every redirect method) but still carry OIDC in their names and + i18n. Rename with a settings migration if wanted. Owner: D0. +- **Unrelated, found in C2:** `node scripts/check-shell-plugin-ids.cjs` + fails on `src/ui/lib/host-to-ssh-host.ts` naming `"web-endpoint"` + (`h.pluginSettings?.["web-endpoint"]`). The file is untouched since before + C2. Owner: D0. ## Manual checks after 2.9.0 @@ -539,3 +560,15 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite plugin and a TOTP user's login is refused until an admin resets their factors; register a passkey, sign in with it with and without a PIN (no PIN still asks for TOTP), delete it; existing 2.8 passkeys still sign in. +- SSO and LDAP: on a 2.8 database every SSO provider and LDAP directory is + there, in Settings > Plugins > Single sign-on and > LDAP; an existing + provider still signs in through its identity provider without changing + anything there (its redirect URI shows `/users/oidc/callback`); add the + new redirect URI at the provider, switch it over and sign in again; a new + provider, GitHub and Google; an existing SSO user and an existing LDAP + user land in their old accounts; the env-configured provider; admin group + and role map; back-channel logout from Keycloak ends the session; the + desktop app's system browser login and Termix-Mobile's login; silent + sign-in; a second factor after an external login with the admin setting + on and off; disable each plugin and its buttons go away and its URLs stop + signing anyone in, then enable it again. diff --git a/packages/plugin-sdk/src/backend.ts b/packages/plugin-sdk/src/backend.ts index 9f294d0d5..cc83f700f 100644 --- a/packages/plugin-sdk/src/backend.ts +++ b/packages/plugin-sdk/src/backend.ts @@ -313,6 +313,12 @@ export interface PluginHttp { * on deactivate. */ router: (options?: PluginRouterOptions) => T; + /** + * The public base URL a request came in on: origin plus the install's base + * path, no trailing slash. Honours forwarded headers from a trusted proxy, + * BASE_PATH and OIDC_FORCE_HTTPS. For building absolute callback URLs. + */ + baseUrl: (req: unknown) => string; } /** @@ -1102,6 +1108,33 @@ export type PluginVerifiedIdentity = /** Redirect methods: where the browser goes back to. */ returnTo?: string; rememberMe?: boolean; + /** + * Provider groups mapped to Termix roles. Core adds the desired roles + * and removes managed roles the user no longer maps to, on every login. + * Roles outside `managed` are never touched. + */ + roles?: { desired: string[]; managed: string[] }; + /** + * Stored on the session so `ctx.auth.revokeSessions` can find it later, + * for an identity provider's back-channel logout. + */ + logoutClaims?: { + providerId?: number | null; + sub?: string | null; + sid?: string | null; + }; + /** + * The values a 2.8 install kept on the user row (`oidc_identifier`, + * `sso_provider_id`). Only for a plugin that took over a 2.8 login + * method: core finds pre-2.9 accounts by `identifier` and keeps writing + * both for new users so a downgrade still works. + */ + legacy?: { identifier: string; providerRowId?: number | null }; + /** + * The key this attempt was counted under with + * `ctx.auth.loginRateLimit.recordFailure`. Cleared on success. + */ + rateLimitKey?: string; }; /** Request shape a login method sees. Structural, like PluginRequestLike. */ @@ -1118,6 +1151,11 @@ export interface PluginLoginInstance { id: string; label: string; enabled: boolean; + /** + * Provider type shown to 2.8 clients by the old `GET /users/sso-providers` + * route ("oidc", "github", "google", "ldap"). Leave unset otherwise. + */ + type?: string; } export interface PluginLoginMethod { @@ -1184,6 +1222,46 @@ export interface PluginAuth { */ recordEnrollment: (userId: string, factorId: string) => Promise; removeEnrollment: (userId: string, factorId: string) => Promise; + /** + * Finishes a redirect login from one of this plugin's own public routes: + * runs the registered method's `callback`, then core finds or provisions + * the user, runs second factors and redirects the browser back with a + * session, exactly like `/users/auth//callback`. The method must + * be one this plugin registered. + */ + completeRedirectLogin: ( + methodId: string, + req: unknown, + res: unknown, + ) => Promise; + /** + * Revokes sessions whose login carried matching `logoutClaims`, for a + * back-channel logout. Needs `sub` or `sid`; `providerId` narrows it. + * Returns how many sessions ended. + */ + revokeSessions: (match: { + providerId?: number | null; + sub?: string | null; + sid?: string | null; + }) => Promise; + /** + * Core's login rate limiter, the one password login uses. Keys are kept + * apart per plugin. Put the key in the identity's `rateLimitKey` so a + * successful login clears it. + */ + loginRateLimit: { + isLocked: ( + ip: string, + key: string, + ) => Promise<{ locked: boolean; remainingTime?: number }>; + recordFailure: (ip: string, key: string) => Promise; + }; + /** + * How many users are linked to an external identity from `provider` (the + * `provider` of the identities this plugin returns). For refusing to + * delete a provider people still sign in with. + */ + countLinkedUsers: (provider: string) => Promise; } export interface PluginOpenIsolatedWindowRequest { diff --git a/packages/plugin-sdk/src/testing.ts b/packages/plugin-sdk/src/testing.ts index 99dfcae69..767db317f 100644 --- a/packages/plugin-sdk/src/testing.ts +++ b/packages/plugin-sdk/src/testing.ts @@ -25,6 +25,13 @@ import type { PluginWebSocketOptions, } from "./backend.js"; +/** The bits of an express Response the fake completeRedirectLogin uses. */ +interface FakeResponse { + status: (code: number) => FakeResponse; + json: (body: unknown) => unknown; + redirect: (url: string) => unknown; +} + /** A ctx.ws registration as the test doubles record it. */ export interface FakeWsRoute { path: string; @@ -37,6 +44,8 @@ export interface FakeWsRoute { } import type { PluginLoginMethod, + PluginLoginRequest, + PluginVerifiedIdentity, PluginSecondFactor, PluginSshAuthProvider, PluginSshConnectOptions, @@ -123,6 +132,12 @@ export interface FakeContextOptions { * proxy login on, password login off), with this message and a 409. */ refuseEnrollment?: string; + /** What ctx.http.baseUrl answers. Defaults to "https://termix.test". */ + baseUrl?: string; + /** What ctx.auth.countLinkedUsers answers, by provider. Defaults to 0. */ + linkedUsers?: Record; + /** Failures before ctx.auth.loginRateLimit locks a key. Defaults to 5. */ + loginAttemptLimit?: number; } export interface FakeAuthRegistrations { @@ -136,6 +151,19 @@ export interface FakeAuthRegistrations { string, (userId: string, reference: string) => Promise >; + /** Identities handed to ctx.auth.completeRedirectLogin, in order. */ + completedLogins: Array<{ + methodId: string; + identity: PluginVerifiedIdentity; + }>; + /** Every ctx.auth.revokeSessions call, in order. */ + revokedSessions: Array<{ + providerId?: number | null; + sub?: string | null; + sid?: string | null; + }>; + /** Failures recorded through ctx.auth.loginRateLimit, by "|". */ + loginFailures: Map; } export interface FakePluginContext { @@ -347,6 +375,9 @@ export function createFakeContext( secondFactors: [], enrollments: new Set(), secretResolvers: new Map(), + completedLogins: [], + revokedSessions: [], + loginFailures: new Map(), }; const sshClient = options.sshClient ?? {}; let actor = options.actor; @@ -531,6 +562,7 @@ export function createFakeContext( httpRouters.push(routerOptions); return (options.router ? options.router() : undefined) as never; }, + baseUrl: () => options.baseUrl ?? "https://termix.test", }, ws: { @@ -801,6 +833,56 @@ export function createFakeContext( removeEnrollment: async (userId, factorId) => { auth.enrollments.delete(`${userId}:${factorId}`); }, + // Stands in for core's pipeline: answers with the identity as JSON, or + // redirects back with the error the way core does. + completeRedirectLogin: async (methodId, req, res) => { + const response = res as FakeResponse; + const method = auth.loginMethods.find( + (candidate) => candidate.id === methodId, + ); + if (!method?.callback) { + throw new Error(`No registered redirect method "${methodId}"`); + } + try { + const identity = await method.callback(req as PluginLoginRequest); + auth.completedLogins.push({ methodId, identity }); + response.status(200).json({ completed: methodId, identity }); + } catch (error) { + const returnTo = (error as { returnTo?: string }).returnTo; + const message = (error as Error).message; + if (returnTo) { + const url = new URL(returnTo); + url.searchParams.set( + "error", + (error as { code?: string }).code ?? message, + ); + response.redirect(url.toString()); + return; + } + response + .status((error as { status?: number }).status ?? 500) + .json({ error: message }); + } + }, + revokeSessions: async (match) => { + if (!match.sub && !match.sid) return 0; + auth.revokedSessions.push(match); + return 1; + }, + loginRateLimit: { + isLocked: async (ip, key) => { + const failures = auth.loginFailures.get(`${ip}|${key}`) ?? 0; + return failures >= (options.loginAttemptLimit ?? 5) + ? { locked: true, remainingTime: 60 } + : { locked: false }; + }, + recordFailure: async (ip, key) => { + const id = `${ip}|${key}`; + auth.loginFailures.set(id, (auth.loginFailures.get(id) ?? 0) + 1); + }, + }, + countLinkedUsers: async (provider) => + options.linkedUsers?.[provider] ?? 0, }, desktop: { @@ -943,6 +1025,12 @@ export interface MockContextOptions { fetch?: (url: string, init?: PluginFetchInit) => Promise; /** See FakeContextOptions. */ refuseEnrollment?: string; + /** See FakeContextOptions. */ + baseUrl?: string; + /** See FakeContextOptions. */ + linkedUsers?: Record; + /** See FakeContextOptions. */ + loginAttemptLimit?: number; } export interface MockPluginContext extends FakePluginContext { @@ -986,6 +1074,9 @@ export function createMockCtx( notificationChannels: options.notificationChannels, fetch: options.fetch, refuseEnrollment: options.refuseEnrollment, + baseUrl: options.baseUrl, + linkedUsers: options.linkedUsers, + loginAttemptLimit: options.loginAttemptLimit, manifest: { capabilities: options.capabilities ?? [], ...options.manifest, @@ -1078,6 +1169,7 @@ export function createMockCtx( require("network:serve"); return ctx.http.router(routerOptions); }, + baseUrl: (req) => ctx.http.baseUrl(req), }, ws: { @@ -1233,6 +1325,28 @@ export function createMockCtx( require("auth:provide"); return ctx.auth.removeEnrollment(userId, factorId); }, + completeRedirectLogin: async (methodId, req, res) => { + require("auth:provide"); + return ctx.auth.completeRedirectLogin(methodId, req, res); + }, + revokeSessions: async (match) => { + require("auth:provide"); + return ctx.auth.revokeSessions(match); + }, + loginRateLimit: { + isLocked: async (ip, key) => { + require("auth:provide"); + return ctx.auth.loginRateLimit.isLocked(ip, key); + }, + recordFailure: async (ip, key) => { + require("auth:provide"); + return ctx.auth.loginRateLimit.recordFailure(ip, key); + }, + }, + countLinkedUsers: async (provider) => { + require("auth:provide"); + return ctx.auth.countLinkedUsers(provider); + }, }, desktop: { diff --git a/plugins/ldap/locales/en.json b/plugins/ldap/locales/en.json new file mode 100644 index 000000000..cf754c641 --- /dev/null +++ b/plugins/ldap/locales/en.json @@ -0,0 +1,50 @@ +{ + "loginWithLdap": "Sign in with LDAP", + "loginWithProvider": "Login with {{name}}", + "loading": "Loading...", + "requiredField": "This field is required", + "username": "LDAP Username", + "password": "LDAP Password", + "signIn": "Sign In", + "permissions": { + "manage": { + "title": "Manage LDAP directories", + "description": "Add, change and remove the LDAP directories people can sign in with." + } + }, + "providers": { + "docsLink": "View docs", + "none": "No LDAP directories configured.", + "add": "Add Directory", + "edit": "Edit Directory", + "delete": "Delete Directory", + "deleteConfirm": "Delete this directory? Users who sign in with it will not be able to log in.", + "name": "Display Name", + "namePlaceholder": "Company directory", + "nameRequired": "A display name is required", + "enabled": "Enabled", + "save": "Save Directory", + "saving": "Saving...", + "saved": "Saved", + "saveFailed": "Failed to save", + "deleted": "Deleted", + "deleteFailed": "Failed to delete", + "loadFailed": "Failed to load LDAP directories", + "cancel": "Cancel" + }, + "fields": { + "host": "LDAP Host", + "port": "Port", + "useTls": "Use TLS (LDAPS)", + "bindDn": "Bind DN", + "bindPassword": "Bind Password", + "secretKeep": "A password is saved. Leave this empty to keep it.", + "userSearchBase": "User Search Base", + "userSearchFilter": "User Search Filter", + "usernameAttr": "Username Attribute", + "displayNameAttr": "Display Name Attribute", + "groupSearchBase": "Group Search Base", + "adminGroup": "Admin Group", + "allowedUsers": "Allowed Users" + } +} diff --git a/plugins/ldap/locales/translated/af_ZA.json b/plugins/ldap/locales/translated/af_ZA.json new file mode 100644 index 000000000..b23b892ca --- /dev/null +++ b/plugins/ldap/locales/translated/af_ZA.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "Teken aan met {{name}}", + "loading": "Laai tans...", + "requiredField": "Hierdie veld is verpligtend", + "username": "LDAP-gebruikersnaam", + "password": "LDAP-wagwoord", + "signIn": "Aanmeld", + "providers": { + "docsLink": "Bekyk dokumente", + "name": "Vertoonnaam", + "enabled": "Geaktiveer", + "saving": "Stoor...", + "saved": "Gestoor", + "saveFailed": "Kon nie stoor nie", + "deleted": "Verwyder", + "deleteFailed": "Kon nie verwyder nie", + "cancel": "Kanselleer" + }, + "fields": { + "host": "LDAP-gasheer", + "port": "Hawe", + "useTls": "Gebruik TLS (LDAPS)", + "bindDn": "Bind DN", + "bindPassword": "Bind Wagwoord", + "userSearchBase": "Gebruikersoekbasis", + "userSearchFilter": "Gebruikersoekfilter", + "usernameAttr": "Gebruikersnaam-kenmerk", + "displayNameAttr": "Vertoonnaam-kenmerk", + "groupSearchBase": "Groepsoekbasis", + "adminGroup": "Admingroep", + "allowedUsers": "Toegelate Gebruikers" + } +} diff --git a/plugins/ldap/locales/translated/ar_SA.json b/plugins/ldap/locales/translated/ar_SA.json new file mode 100644 index 000000000..ba88865ab --- /dev/null +++ b/plugins/ldap/locales/translated/ar_SA.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "تسجيل الدخول باستخدام {{name}}", + "loading": "جارٍ التحميل...", + "requiredField": "هذا الحقل مطلوب", + "username": "اسم مستخدم LDAP", + "password": "كلمة مرور LDAP", + "signIn": "تسجيل الدخول", + "providers": { + "docsLink": "عرض المستندات", + "name": "الاسم المعروض", + "enabled": "مُمكّن", + "saving": "جارٍ الحفظ...", + "saved": "محفوظ", + "saveFailed": "فشل في الحفظ", + "deleted": "محذوف", + "deleteFailed": "فشل في الحذف", + "cancel": "إلغاء" + }, + "fields": { + "host": "مضيف LDAP", + "port": "المنفذ", + "useTls": "استخدام TLS (LDAPS)", + "bindDn": "اسم مميز للربط", + "bindPassword": "كلمة مرور الارتباط", + "userSearchBase": "قاعدة بحث المستخدمين", + "userSearchFilter": "مرشح بحث المستخدمين", + "usernameAttr": "سمة اسم المستخدم", + "displayNameAttr": "سمة اسم العرض", + "groupSearchBase": "قاعدة بحث المجموعات", + "adminGroup": "مجموعة المديرين", + "allowedUsers": "المستخدمون المسموح بهم" + } +} diff --git a/plugins/ldap/locales/translated/bg_BG.json b/plugins/ldap/locales/translated/bg_BG.json new file mode 100644 index 000000000..12a036d46 --- /dev/null +++ b/plugins/ldap/locales/translated/bg_BG.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "Влезте с {{name}}", + "loading": "Зареждане...", + "requiredField": "Това поле е задължително", + "username": "Потребителско име на LDAP", + "password": "LDAP парола", + "signIn": "Вход", + "providers": { + "docsLink": "Преглед на документи", + "name": "Показвано име", + "enabled": "Активирано", + "saving": "Запазване...", + "saved": "Запазено", + "saveFailed": "Запазването не бе успешно", + "deleted": "Изтрито", + "deleteFailed": "Изтриването не бе успешно", + "cancel": "Отказ" + }, + "fields": { + "host": "LDAP хост", + "port": "Порт", + "useTls": "Използвайте TLS (LDAPS)", + "bindDn": "Обвързване DN", + "bindPassword": "Свързване на парола", + "userSearchBase": "База за търсене на потребители", + "userSearchFilter": "Филтър за търсене на потребители", + "usernameAttr": "Атрибут на потребителското име", + "displayNameAttr": "Атрибут за показвано име", + "groupSearchBase": "База за групово търсене", + "adminGroup": "Административна група", + "allowedUsers": "Разрешени потребители" + } +} diff --git a/plugins/ldap/locales/translated/bn_BD.json b/plugins/ldap/locales/translated/bn_BD.json new file mode 100644 index 000000000..c1e0f0764 --- /dev/null +++ b/plugins/ldap/locales/translated/bn_BD.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "{{name}} দিয়ে লগইন করুন", + "loading": "লোড হচ্ছে...", + "requiredField": "এই ক্ষেত্রটি আবশ্যক", + "username": "LDAP ব্যবহারকারীর নাম", + "password": "এলডিএপি পাসওয়ার্ড", + "signIn": "সাইন ইন করুন", + "providers": { + "docsLink": "নথি দেখুন", + "name": "প্রদর্শনের নাম", + "enabled": "সক্ষম করা হয়েছে", + "saving": "সঞ্চয়...", + "saved": "সংরক্ষিত", + "saveFailed": "সংরক্ষণ করতে ব্যর্থ হয়েছে", + "deleted": "মুছে ফেলা হয়েছে", + "deleteFailed": "মুছে ফেলতে ব্যর্থ হয়েছে", + "cancel": "বাতিল করুন" + }, + "fields": { + "host": "এলডিএপি হোস্ট", + "port": "বন্দর", + "useTls": "TLS (LDAPS) ব্যবহার করুন", + "bindDn": "ডিএনকে আবদ্ধ করুন", + "bindPassword": "পাসওয়ার্ড সংযুক্ত করুন", + "userSearchBase": "ব্যবহারকারী অনুসন্ধান ভিত্তি", + "userSearchFilter": "ব্যবহারকারী অনুসন্ধান ফিল্টার", + "usernameAttr": "ব্যবহারকারীর নাম অ্যাট্রিবিউট", + "displayNameAttr": "ডিসপ্লে নেম অ্যাট্রিবিউট", + "groupSearchBase": "গ্রুপ অনুসন্ধান ভিত্তি", + "adminGroup": "অ্যাডমিন গ্রুপ", + "allowedUsers": "অনুমোদিত ব্যবহারকারী" + } +} diff --git a/plugins/ldap/locales/translated/ca_ES.json b/plugins/ldap/locales/translated/ca_ES.json new file mode 100644 index 000000000..e4d6a4873 --- /dev/null +++ b/plugins/ldap/locales/translated/ca_ES.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "Inicia la sessió amb {{name}}", + "loading": "S'està carregant...", + "requiredField": "Aquest camp és obligatori", + "username": "Nom d'usuari LDAP", + "password": "Contrasenya LDAP", + "signIn": "Inicia la sessió", + "providers": { + "docsLink": "Veure documents", + "name": "Nom de visualització", + "enabled": "Activat", + "saving": "Desant...", + "saved": "Desat", + "saveFailed": "No s'ha pogut desar", + "deleted": "Suprimit", + "deleteFailed": "No s'ha pogut suprimir", + "cancel": "Cancel·la" + }, + "fields": { + "host": "Amfitrió LDAP", + "port": "Port", + "useTls": "Utilitza TLS (LDAPS)", + "bindDn": "Vinculació de nom de domini", + "bindPassword": "Contrasenya de vinculació", + "userSearchBase": "Base de cerca d'usuaris", + "userSearchFilter": "Filtre de cerca d'usuaris", + "usernameAttr": "Atribut de nom d'usuari", + "displayNameAttr": "Atribut del nom de visualització", + "groupSearchBase": "Base de cerca de grups", + "adminGroup": "Grup d'administradors", + "allowedUsers": "Usuaris permesos" + } +} diff --git a/plugins/ldap/locales/translated/cs_CZ.json b/plugins/ldap/locales/translated/cs_CZ.json new file mode 100644 index 000000000..68ce59377 --- /dev/null +++ b/plugins/ldap/locales/translated/cs_CZ.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "Přihlaste se pomocí {{name}}", + "loading": "Načítání...", + "requiredField": "Toto pole je povinné", + "username": "Uživatelské jméno LDAP", + "password": "Heslo LDAP", + "signIn": "Přihlásit se", + "providers": { + "docsLink": "Zobrazit dokumenty", + "name": "Zobrazovaný název", + "enabled": "Povoleno", + "saving": "Ukládání...", + "saved": "Uloženo", + "saveFailed": "Uložení se nezdařilo", + "deleted": "Smazáno", + "deleteFailed": "Nepodařilo se smazat", + "cancel": "Zrušit" + }, + "fields": { + "host": "Hostitel LDAP", + "port": "Přístav", + "useTls": "Používejte TLS (LDAPS)", + "bindDn": "Vázat DN", + "bindPassword": "Vázat heslo", + "userSearchBase": "Vyhledávací báze uživatelů", + "userSearchFilter": "Filtr vyhledávání uživatelů", + "usernameAttr": "Atribut uživatelského jména", + "displayNameAttr": "Atribut zobrazovaného názvu", + "groupSearchBase": "Základna pro vyhledávání skupin", + "adminGroup": "Skupina administrátorů", + "allowedUsers": "Povolení uživatelé" + } +} diff --git a/plugins/ldap/locales/translated/da_DK.json b/plugins/ldap/locales/translated/da_DK.json new file mode 100644 index 000000000..f57bc26b6 --- /dev/null +++ b/plugins/ldap/locales/translated/da_DK.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "Log ind med {{name}}", + "loading": "Indlæser...", + "requiredField": "Dette felt er påkrævet", + "username": "LDAP-brugernavn", + "password": "LDAP-adgangskode", + "signIn": "Log ind", + "providers": { + "docsLink": "Se dokumenter", + "name": "Vist navn", + "enabled": "Aktiveret", + "saving": "Gemmer...", + "saved": "Gemt", + "saveFailed": "Kunne ikke gemme", + "deleted": "Slettet", + "deleteFailed": "Kunne ikke slette", + "cancel": "Ophæve" + }, + "fields": { + "host": "LDAP-vært", + "port": "Havn", + "useTls": "Brug TLS (LDAPS)", + "bindDn": "Bind DN", + "bindPassword": "Bind adgangskode", + "userSearchBase": "Brugersøgningsbase", + "userSearchFilter": "Brugersøgningsfilter", + "usernameAttr": "Brugernavnsattribut", + "displayNameAttr": "Visningsnavnattribut", + "groupSearchBase": "Gruppesøgningsbase", + "adminGroup": "Admingruppe", + "allowedUsers": "Tilladte brugere" + } +} diff --git a/plugins/ldap/locales/translated/de_DE.json b/plugins/ldap/locales/translated/de_DE.json new file mode 100644 index 000000000..e012d5d84 --- /dev/null +++ b/plugins/ldap/locales/translated/de_DE.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "Anmeldung mit {{name}}", + "loading": "Laden...", + "requiredField": "Dieses Feld ist erforderlich", + "username": "LDAP-Benutzername", + "password": "LDAP-Passwort", + "signIn": "Anmelden", + "providers": { + "docsLink": "Dokumentation anzeigen", + "name": "Anzeigename", + "enabled": "Aktiviert", + "saving": "Speichern...", + "saved": "Gespeichert", + "saveFailed": "Speichern fehlgeschlagen", + "deleted": "Gelöscht", + "deleteFailed": "Löschen fehlgeschlagen", + "cancel": "Abbrechen" + }, + "fields": { + "host": "LDAP-Host", + "port": "Port", + "useTls": "TLS verwenden (LDAPS)", + "bindDn": "Bind-DN", + "bindPassword": "Bind-Passwort", + "userSearchBase": "Benutzer-Suchbasis", + "userSearchFilter": "Benutzersuchfilter", + "usernameAttr": "Benutzernamen-Attribut", + "displayNameAttr": "Anzeigename-Attribut", + "groupSearchBase": "Gruppen-Suchbasis", + "adminGroup": "Admin-Gruppe", + "allowedUsers": "Erlaubte Benutzer" + } +} diff --git a/plugins/ldap/locales/translated/el_GR.json b/plugins/ldap/locales/translated/el_GR.json new file mode 100644 index 000000000..67e06a222 --- /dev/null +++ b/plugins/ldap/locales/translated/el_GR.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "Σύνδεση με {{name}}", + "loading": "Φόρτωση...", + "requiredField": "Αυτό το πεδίο είναι υποχρεωτικό", + "username": "Όνομα χρήστη LDAP", + "password": "Κωδικός πρόσβασης LDAP", + "signIn": "Σύνδεση", + "providers": { + "docsLink": "Προβολή εγγράφων", + "name": "Εμφανιζόμενο όνομα", + "enabled": "Ενεργοποιημένο", + "saving": "Οικονομία...", + "saved": "Αποθηκεύτηκε", + "saveFailed": "Η αποθήκευση απέτυχε", + "deleted": "Διαγράφηκε", + "deleteFailed": "Η διαγραφή απέτυχε", + "cancel": "Ματαίωση" + }, + "fields": { + "host": "Κεντρικός υπολογιστής LDAP", + "port": "Λιμάνι", + "useTls": "Χρήση TLS (LDAPS)", + "bindDn": "Σύνδεση DN", + "bindPassword": "Σύνδεση κωδικού πρόσβασης", + "userSearchBase": "Βάση αναζήτησης χρηστών", + "userSearchFilter": "Φίλτρο αναζήτησης χρήστη", + "usernameAttr": "Χαρακτηριστικό ονόματος χρήστη", + "displayNameAttr": "Χαρακτηριστικό ονόματος εμφάνισης", + "groupSearchBase": "Βάση αναζήτησης ομάδας", + "adminGroup": "Ομάδα Διαχειριστών", + "allowedUsers": "Επιτρεπόμενοι χρήστες" + } +} diff --git a/plugins/ldap/locales/translated/es_ES.json b/plugins/ldap/locales/translated/es_ES.json new file mode 100644 index 000000000..68bc316f8 --- /dev/null +++ b/plugins/ldap/locales/translated/es_ES.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "Iniciar sesión con {{name}}", + "loading": "Cargando...", + "requiredField": "Este campo es obligatorio", + "username": "Nombre de usuario LDAP", + "password": "Contraseña LDAP", + "signIn": "Iniciar sesión", + "providers": { + "docsLink": "Ver documentación", + "name": "Nombre visible", + "enabled": "Activado", + "saving": "Guardando...", + "saved": "Guardado", + "saveFailed": "Error al guardar", + "deleted": "Eliminado", + "deleteFailed": "Error al eliminar", + "cancel": "Cancelar" + }, + "fields": { + "host": "Host LDAP", + "port": "Puerto", + "useTls": "Usar TLS (LDAPS)", + "bindDn": "DN de enlace", + "bindPassword": "Contraseña de enlace", + "userSearchBase": "Base de búsqueda de usuarios", + "userSearchFilter": "Filtro de búsqueda de usuarios", + "usernameAttr": "Atributo de nombre de usuario", + "displayNameAttr": "Atributo de nombre visible", + "groupSearchBase": "Base de búsqueda de grupos", + "adminGroup": "Grupo de administradores", + "allowedUsers": "Usuarios permitidos" + } +} diff --git a/plugins/ldap/locales/translated/fi_FI.json b/plugins/ldap/locales/translated/fi_FI.json new file mode 100644 index 000000000..14b7fba19 --- /dev/null +++ b/plugins/ldap/locales/translated/fi_FI.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "Kirjaudu sisään tunnuksella {{name}}", + "loading": "Ladataan...", + "requiredField": "Tämä kenttä on pakollinen", + "username": "LDAP-käyttäjätunnus", + "password": "LDAP-salasana", + "signIn": "Kirjaudu sisään", + "providers": { + "docsLink": "Näytä dokumentit", + "name": "Näyttönimi", + "enabled": "Käytössä", + "saving": "Tallennetaan...", + "saved": "Tallennettu", + "saveFailed": "Tallennus epäonnistui", + "deleted": "Poistettu", + "deleteFailed": "Poistaminen epäonnistui", + "cancel": "Peruuttaa" + }, + "fields": { + "host": "LDAP-isäntä", + "port": "Portti", + "useTls": "Käytä TLS:ää (LDAPS)", + "bindDn": "Sido DN", + "bindPassword": "Sido salasana", + "userSearchBase": "Käyttäjähakupohja", + "userSearchFilter": "Käyttäjän hakusuodatin", + "usernameAttr": "Käyttäjätunnusattribuutti", + "displayNameAttr": "Näyttönimen attribuutti", + "groupSearchBase": "Ryhmän hakukanta", + "adminGroup": "Hallintaryhmä", + "allowedUsers": "Sallitut käyttäjät" + } +} diff --git a/plugins/ldap/locales/translated/fr_FR.json b/plugins/ldap/locales/translated/fr_FR.json new file mode 100644 index 000000000..a363695e7 --- /dev/null +++ b/plugins/ldap/locales/translated/fr_FR.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "Se connecter avec {{name}}", + "loading": "Chargement...", + "requiredField": "Ce champ est obligatoire", + "username": "Nom d'utilisateur LDAP", + "password": "Mot de passe LDAP", + "signIn": "Se connecter", + "providers": { + "docsLink": "Documentation", + "name": "Nom d’affichage", + "enabled": "Activé", + "saving": "Enregistrement…", + "saved": "Enregistré", + "saveFailed": "Échec de l'enregistrement", + "deleted": "Supprimé", + "deleteFailed": "Échec de la suppression", + "cancel": "Annuler" + }, + "fields": { + "host": "Hôte LDAP", + "port": "Port", + "useTls": "Utiliser TLS (LDAPS)", + "bindDn": "DN de liaison", + "bindPassword": "Mot de passe de liaison", + "userSearchBase": "Base de recherche utilisateur", + "userSearchFilter": "Filtre de recherche utilisateur", + "usernameAttr": "Attribut de nom d’utilisateur", + "displayNameAttr": "Attribut de nom d’affichage", + "groupSearchBase": "Base de recherche de groupe", + "adminGroup": "Groupe admin", + "allowedUsers": "Utilisateurs autorisés" + } +} diff --git a/plugins/ldap/locales/translated/he_IL.json b/plugins/ldap/locales/translated/he_IL.json new file mode 100644 index 000000000..37e06cc87 --- /dev/null +++ b/plugins/ldap/locales/translated/he_IL.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "התחברות באמצעות {{name}}", + "loading": "טְעִינָה...", + "requiredField": "שדה זה נדרש", + "username": "שם משתמש LDAP", + "password": "סיסמת LDAP", + "signIn": "כניסה", + "providers": { + "docsLink": "הצג מסמכים", + "name": "שם תצוגה", + "enabled": "מופעל", + "saving": "חִסָכוֹן...", + "saved": "נשמר", + "saveFailed": "השמירה נכשלה", + "deleted": "נמחק", + "deleteFailed": "המחיקה נכשלה", + "cancel": "לְבַטֵל" + }, + "fields": { + "host": "מארח LDAP", + "port": "נָמָל", + "useTls": "השתמש ב-TLS (LDAPS)", + "bindDn": "קשר DN", + "bindPassword": "קשירת סיסמה", + "userSearchBase": "בסיס חיפוש משתמשים", + "userSearchFilter": "מסנן חיפוש משתמשים", + "usernameAttr": "מאפיין שם משתמש", + "displayNameAttr": "מאפיין שם תצוגה", + "groupSearchBase": "בסיס חיפוש קבוצתי", + "adminGroup": "קבוצת ניהול", + "allowedUsers": "משתמשים מורשים" + } +} diff --git a/plugins/ldap/locales/translated/hi_IN.json b/plugins/ldap/locales/translated/hi_IN.json new file mode 100644 index 000000000..1be9fd202 --- /dev/null +++ b/plugins/ldap/locales/translated/hi_IN.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "{{name}} से लॉगिन करें", + "loading": "लोड हो रहा है...", + "requiredField": "यह फ़ील्ड आवश्यक है", + "username": "LDAP उपयोगकर्ता नाम", + "password": "LDAP पासवर्ड", + "signIn": "साइन इन करें", + "providers": { + "docsLink": "दस्तावेज़ देखें", + "name": "प्रदर्शन नाम", + "enabled": "सक्षम", + "saving": "सहेजा जा रहा है...", + "saved": "सहेजा गया", + "saveFailed": "सहेजने में विफल", + "deleted": "हटाया गया", + "deleteFailed": "हटाने में विफल", + "cancel": "रद्द करें" + }, + "fields": { + "host": "LDAP होस्ट", + "port": "पोर्ट", + "useTls": "TLS (LDAPS) का उपयोग करें", + "bindDn": "Bind DN", + "bindPassword": "Bind पासवर्ड", + "userSearchBase": "उपयोगकर्ता खोज आधार", + "userSearchFilter": "उपयोगकर्ता खोज फ़िल्टर", + "usernameAttr": "उपयोगकर्ता नाम विशेषता", + "displayNameAttr": "प्रदर्शन नाम विशेषता", + "groupSearchBase": "समूह खोज आधार", + "adminGroup": "एडमिन समूह", + "allowedUsers": "अनुमत उपयोगकर्ता" + } +} diff --git a/plugins/ldap/locales/translated/hu_HU.json b/plugins/ldap/locales/translated/hu_HU.json new file mode 100644 index 000000000..4232a895d --- /dev/null +++ b/plugins/ldap/locales/translated/hu_HU.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "Bejelentkezés {{name}} használatával", + "loading": "Terhelés...", + "requiredField": "Ez a mező kötelező", + "username": "LDAP felhasználónév", + "password": "LDAP jelszó", + "signIn": "Bejelentkezés", + "providers": { + "docsLink": "Dokumentumok megtekintése", + "name": "Megjelenített név", + "enabled": "Engedélyezve", + "saving": "Megtakarítás...", + "saved": "Mentett", + "saveFailed": "Nem sikerült menteni", + "deleted": "Törölt", + "deleteFailed": "Nem sikerült törölni", + "cancel": "Mégsem" + }, + "fields": { + "host": "LDAP-gazdagép", + "port": "Kikötő", + "useTls": "TLS (LDAPS) használata", + "bindDn": "Kötési DN", + "bindPassword": "Jelszó kötése", + "userSearchBase": "Felhasználói keresési bázis", + "userSearchFilter": "Felhasználói keresési szűrő", + "usernameAttr": "Felhasználónév attribútum", + "displayNameAttr": "Megjelenített név attribútum", + "groupSearchBase": "Csoportkeresési bázis", + "adminGroup": "Adminisztrátori csoport", + "allowedUsers": "Engedélyezett felhasználók" + } +} diff --git a/plugins/ldap/locales/translated/id_ID.json b/plugins/ldap/locales/translated/id_ID.json new file mode 100644 index 000000000..8d68e19de --- /dev/null +++ b/plugins/ldap/locales/translated/id_ID.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "Masuk dengan {{name}}", + "loading": "Memuat...", + "requiredField": "Kolom ini wajib diisi.", + "username": "Nama Pengguna LDAP", + "password": "Kata Sandi LDAP", + "signIn": "Masuk", + "providers": { + "docsLink": "Lihat dokumen", + "name": "Nama Tampilan", + "enabled": "Diaktifkan", + "saving": "Penghematan...", + "saved": "Tersimpan", + "saveFailed": "Gagal menyimpan", + "deleted": "Dihapus", + "deleteFailed": "Gagal menghapus", + "cancel": "Membatalkan" + }, + "fields": { + "host": "Host LDAP", + "port": "Pelabuhan", + "useTls": "Gunakan TLS (LDAPS)", + "bindDn": "Ikat DN", + "bindPassword": "Kata Sandi Terikat", + "userSearchBase": "Basis Pencarian Pengguna", + "userSearchFilter": "Filter Pencarian Pengguna", + "usernameAttr": "Atribut Nama Pengguna", + "displayNameAttr": "Atribut Nama Tampilan", + "groupSearchBase": "Basis Pencarian Grup", + "adminGroup": "Grup Admin", + "allowedUsers": "Pengguna yang Diizinkan" + } +} diff --git a/plugins/ldap/locales/translated/it_IT.json b/plugins/ldap/locales/translated/it_IT.json new file mode 100644 index 000000000..dd8dd466e --- /dev/null +++ b/plugins/ldap/locales/translated/it_IT.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "Accedi con {{name}}", + "loading": "Caricamento in corso...", + "requiredField": "Questo campo è obbligatorio", + "username": "Nome utente LDAP", + "password": "Password LDAP", + "signIn": "Accedi", + "providers": { + "docsLink": "Visualizza documentazione", + "name": "Nome visualizzato", + "enabled": "Abilitato", + "saving": "Salvataggio in corso...", + "saved": "Salvato", + "saveFailed": "Salvataggio non riuscito", + "deleted": "Eliminato", + "deleteFailed": "Eliminazione non riuscita", + "cancel": "Annulla" + }, + "fields": { + "host": "Host LDAP", + "port": "Porta", + "useTls": "Usa TLS (LDAPS)", + "bindDn": "Bind DN", + "bindPassword": "Password di bind", + "userSearchBase": "Base di ricerca utenti", + "userSearchFilter": "Filtro di ricerca utenti", + "usernameAttr": "Attributo nome utente", + "displayNameAttr": "Attributo nome visualizzato", + "groupSearchBase": "Base di ricerca gruppi", + "adminGroup": "Gruppo amministratori", + "allowedUsers": "Utenti autorizzati" + } +} diff --git a/plugins/ldap/locales/translated/ja_JP.json b/plugins/ldap/locales/translated/ja_JP.json new file mode 100644 index 000000000..66c63757b --- /dev/null +++ b/plugins/ldap/locales/translated/ja_JP.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "{{name}}でログイン", + "loading": "読み込み中...", + "requiredField": "この項目は必須です", + "username": "LDAPユーザー名", + "password": "LDAPパスワード", + "signIn": "サインイン", + "providers": { + "docsLink": "ドキュメントを表示", + "name": "表示名", + "enabled": "有効", + "saving": "保存中...", + "saved": "保存しました", + "saveFailed": "保存に失敗しました", + "deleted": "削除しました", + "deleteFailed": "削除に失敗しました", + "cancel": "キャンセル" + }, + "fields": { + "host": "LDAPホスト", + "port": "ポート", + "useTls": "TLS (LDAPS) を使用", + "bindDn": "バインドDN", + "bindPassword": "バインドパスワード", + "userSearchBase": "ユーザー検索ベース", + "userSearchFilter": "ユーザー検索フィルター", + "usernameAttr": "ユーザー名属性", + "displayNameAttr": "表示名属性", + "groupSearchBase": "グループ検索ベース", + "adminGroup": "管理者グループ", + "allowedUsers": "許可ユーザー" + } +} diff --git a/plugins/ldap/locales/translated/ko_KR.json b/plugins/ldap/locales/translated/ko_KR.json new file mode 100644 index 000000000..319043b25 --- /dev/null +++ b/plugins/ldap/locales/translated/ko_KR.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "{{name}}으로 로그인", + "loading": "로딩 중...", + "requiredField": "필수 입력란입니다", + "username": "LDAP 사용자 이름", + "password": "LDAP 비밀번호", + "signIn": "로그인", + "providers": { + "docsLink": "문서 보기", + "name": "표시 이름", + "enabled": "사용", + "saving": "저장 중...", + "saved": "저장됨", + "saveFailed": "저장 실패", + "deleted": "삭제됨", + "deleteFailed": "삭제 실패", + "cancel": "취소" + }, + "fields": { + "host": "LDAP 호스트", + "port": "포트", + "useTls": "TLS 사용 (LDAPS)", + "bindDn": "바인딩 DN", + "bindPassword": "바인딩 암호", + "userSearchBase": "사용자 검색 기준", + "userSearchFilter": "사용자 검색 필터", + "usernameAttr": "사용자 이름 속성", + "displayNameAttr": "표시 이름 속성", + "groupSearchBase": "그룹 검색 기준", + "adminGroup": "관리자 그룹", + "allowedUsers": "허용 사용자" + } +} diff --git a/plugins/ldap/locales/translated/nl_NL.json b/plugins/ldap/locales/translated/nl_NL.json new file mode 100644 index 000000000..cdc9be93c --- /dev/null +++ b/plugins/ldap/locales/translated/nl_NL.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "Inloggen met {{name}}", + "loading": "Laden...", + "requiredField": "Dit veld is verplicht.", + "username": "LDAP-gebruikersnaam", + "password": "LDAP-wachtwoord", + "signIn": "Inloggen", + "providers": { + "docsLink": "Documenten bekijken", + "name": "Weergavenaam", + "enabled": "Ingeschakeld", + "saving": "Besparing...", + "saved": "Opgeslagen", + "saveFailed": "Opslaan mislukt", + "deleted": "Verwijderd", + "deleteFailed": "Verwijderen mislukt", + "cancel": "Annuleren" + }, + "fields": { + "host": "LDAP-host", + "port": "Haven", + "useTls": "Gebruik TLS (LDAPS)", + "bindDn": "Bind DN", + "bindPassword": "Bind wachtwoord", + "userSearchBase": "Gebruikerszoekbasis", + "userSearchFilter": "Gebruikerszoekfilter", + "usernameAttr": "Gebruikersnaamkenmerk", + "displayNameAttr": "Weergavenaamkenmerk", + "groupSearchBase": "Groepszoekbasis", + "adminGroup": "Beheerdersgroep", + "allowedUsers": "Toegestane gebruikers" + } +} diff --git a/plugins/ldap/locales/translated/no_NO.json b/plugins/ldap/locales/translated/no_NO.json new file mode 100644 index 000000000..b22a3b31d --- /dev/null +++ b/plugins/ldap/locales/translated/no_NO.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "Logg inn med {{name}}", + "loading": "Laster inn...", + "requiredField": "Dette feltet er obligatorisk", + "username": "LDAP-brukernavn", + "password": "LDAP-passord", + "signIn": "Logg inn", + "providers": { + "docsLink": "Vis dokumenter", + "name": "Visningsnavn", + "enabled": "Aktivert", + "saving": "Lagrer...", + "saved": "Lagret", + "saveFailed": "Kunne ikke lagre", + "deleted": "Slettet", + "deleteFailed": "Kunne ikke slette", + "cancel": "Kansellere" + }, + "fields": { + "host": "LDAP-vert", + "port": "Havn", + "useTls": "Bruk TLS (LDAPS)", + "bindDn": "Bind DN", + "bindPassword": "Bind passord", + "userSearchBase": "Brukersøkbase", + "userSearchFilter": "Brukersøkfilter", + "usernameAttr": "Brukernavnattributt", + "displayNameAttr": "Visningsnavnattributt", + "groupSearchBase": "Gruppesøkbase", + "adminGroup": "Admingruppe", + "allowedUsers": "Tillatte brukere" + } +} diff --git a/plugins/ldap/locales/translated/pl_PL.json b/plugins/ldap/locales/translated/pl_PL.json new file mode 100644 index 000000000..0efc8be66 --- /dev/null +++ b/plugins/ldap/locales/translated/pl_PL.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "Zaloguj się za pomocą {{name}}", + "loading": "Ładowanie...", + "requiredField": "To pole jest wymagane", + "username": "Nazwa użytkownika LDAP", + "password": "Hasło LDAP", + "signIn": "Zaloguj się", + "providers": { + "docsLink": "Zobacz dokumentację", + "name": "Nazwa wyświetlana", + "enabled": "Włączone", + "saving": "Zapisywanie...", + "saved": "Zapisano", + "saveFailed": "Nie udało się zapisać", + "deleted": "Usunięto", + "deleteFailed": "Nie udało się usunąć", + "cancel": "Anuluj" + }, + "fields": { + "host": "Host LDAP", + "port": "Port", + "useTls": "Użyj TLS (LDAPS)", + "bindDn": "Bind DN", + "bindPassword": "Hasło Bind", + "userSearchBase": "Baza wyszukiwania użytkowników", + "userSearchFilter": "Filtr wyszukiwania użytkownika", + "usernameAttr": "Atrybut nazwy użytkownika", + "displayNameAttr": "Atrybut nazwy wyświetlanej", + "groupSearchBase": "Baza wyszukiwania grup", + "adminGroup": "Grupa administratorów", + "allowedUsers": "Dozwoleni użytkownicy" + } +} diff --git a/plugins/ldap/locales/translated/pt_BR.json b/plugins/ldap/locales/translated/pt_BR.json new file mode 100644 index 000000000..eed910372 --- /dev/null +++ b/plugins/ldap/locales/translated/pt_BR.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "Faça login com {{name}}", + "loading": "Carregando...", + "requiredField": "Este campo é obrigatório", + "username": "Nome de usuário LDAP", + "password": "Senha LDAP", + "signIn": "Entrar", + "providers": { + "docsLink": "Ver documentos", + "name": "Nome de exibição", + "enabled": "Habilitado", + "saving": "Salvando...", + "saved": "Salvo", + "saveFailed": "Falha ao salvar", + "deleted": "Excluído", + "deleteFailed": "Falha ao excluir", + "cancel": "Cancelar" + }, + "fields": { + "host": "Host LDAP", + "port": "Porta", + "useTls": "Usar TLS (LDAPS)", + "bindDn": "Vincular DN", + "bindPassword": "Senha de vinculação", + "userSearchBase": "Base de pesquisa de usuários", + "userSearchFilter": "Filtro de pesquisa do usuário", + "usernameAttr": "Atributo de nome de usuário", + "displayNameAttr": "Atributo de nome de exibição", + "groupSearchBase": "Base de pesquisa de grupo", + "adminGroup": "Grupo de Administração", + "allowedUsers": "Usuários autorizados" + } +} diff --git a/plugins/ldap/locales/translated/pt_PT.json b/plugins/ldap/locales/translated/pt_PT.json new file mode 100644 index 000000000..edae5f971 --- /dev/null +++ b/plugins/ldap/locales/translated/pt_PT.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "Iniciar sessão com {{name}}", + "loading": "A carregar...", + "requiredField": "Este campo é obrigatório", + "username": "Nome de utilizador LDAP", + "password": "Palavra-passe LDAP", + "signIn": "Iniciar sessão", + "providers": { + "docsLink": "Ver docs", + "name": "Nome de apresentação", + "enabled": "Ativado", + "saving": "A guardar...", + "saved": "Guardado", + "saveFailed": "Falha ao guardar", + "deleted": "Eliminado", + "deleteFailed": "Falha ao eliminar", + "cancel": "Cancelar" + }, + "fields": { + "host": "Servidor LDAP", + "port": "Porta", + "useTls": "Usar TLS (LDAPS)", + "bindDn": "Bind DN", + "bindPassword": "Palavra-passe de Bind", + "userSearchBase": "Base de pesquisa de utilizadores", + "userSearchFilter": "Filtro de pesquisa de utilizadores", + "usernameAttr": "Atributo de nome de utilizador", + "displayNameAttr": "Atributo de nome de apresentação", + "groupSearchBase": "Base de pesquisa de grupos", + "adminGroup": "Grupo de administradores", + "allowedUsers": "Utilizadores permitidos" + } +} diff --git a/plugins/ldap/locales/translated/ro_RO.json b/plugins/ldap/locales/translated/ro_RO.json new file mode 100644 index 000000000..fa38b0b9d --- /dev/null +++ b/plugins/ldap/locales/translated/ro_RO.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "Autentificare cu {{name}}", + "loading": "Încărcare...", + "requiredField": "Acest câmp este obligatoriu", + "username": "Nume de utilizator LDAP", + "password": "Parolă LDAP", + "signIn": "Conectare", + "providers": { + "docsLink": "Vizualizați documentele", + "name": "Numele de afișare", + "enabled": "Activat", + "saving": "Economisire...", + "saved": "Salvat", + "saveFailed": "Salvarea nu a reușit", + "deleted": "Șters", + "deleteFailed": "Ștergerea nu a reușit", + "cancel": "Anula" + }, + "fields": { + "host": "Gazdă LDAP", + "port": "Port", + "useTls": "Utilizați TLS (LDAPS)", + "bindDn": "Legați DN-ul", + "bindPassword": "Parolă de legare", + "userSearchBase": "Baza de căutare a utilizatorilor", + "userSearchFilter": "Filtru de căutare utilizator", + "usernameAttr": "Atribut nume utilizator", + "displayNameAttr": "Atributul numelui afișat", + "groupSearchBase": "Baza de căutare a grupului", + "adminGroup": "Grup de administratori", + "allowedUsers": "Utilizatori permiși" + } +} diff --git a/plugins/ldap/locales/translated/ru_RU.json b/plugins/ldap/locales/translated/ru_RU.json new file mode 100644 index 000000000..5b2f7a327 --- /dev/null +++ b/plugins/ldap/locales/translated/ru_RU.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "Войти через {{name}}", + "loading": "Загрузка...", + "requiredField": "Обязательное поле", + "username": "Имя пользователя LDAP", + "password": "Пароль LDAP", + "signIn": "Войти", + "providers": { + "docsLink": "Документация", + "name": "Отображаемое имя", + "enabled": "Включено", + "saving": "Сохранение...", + "saved": "Сохранено", + "saveFailed": "Не удалось сохранить", + "deleted": "Удалено", + "deleteFailed": "Не удалось удалить", + "cancel": "Отмена" + }, + "fields": { + "host": "LDAP-сервер", + "port": "Порт", + "useTls": "Использовать TLS (LDAPS)", + "bindDn": "Bind DN", + "bindPassword": "Пароль привязки", + "userSearchBase": "База поиска пользователей", + "userSearchFilter": "Фильтр поиска пользователей", + "usernameAttr": "Атрибут имени пользователя", + "displayNameAttr": "Атрибут отображаемого имени", + "groupSearchBase": "База поиска групп", + "adminGroup": "Группа администраторов", + "allowedUsers": "Разрешенные пользователи" + } +} diff --git a/plugins/ldap/locales/translated/sr_SP.json b/plugins/ldap/locales/translated/sr_SP.json new file mode 100644 index 000000000..ea430f864 --- /dev/null +++ b/plugins/ldap/locales/translated/sr_SP.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "Пријавите се са {{name}}", + "loading": "Учитавање...", + "requiredField": "Ово поље је обавезно", + "username": "LDAP корисничко име", + "password": "LDAP лозинка", + "signIn": "Пријавите се", + "providers": { + "docsLink": "Прикажи документе", + "name": "Приказано име", + "enabled": "Омогућено", + "saving": "Чување...", + "saved": "Сачувано", + "saveFailed": "Чување није успело", + "deleted": "Избрисано", + "deleteFailed": "Брисање није успело", + "cancel": "Откажи" + }, + "fields": { + "host": "LDAP хост", + "port": "Лука", + "useTls": "Користите TLS (LDAPS)", + "bindDn": "Повезивање DN-а", + "bindPassword": "Повезивање лозинке", + "userSearchBase": "База претраге корисника", + "userSearchFilter": "Филтер за претрагу корисника", + "usernameAttr": "Атрибут корисничког имена", + "displayNameAttr": "Атрибут приказаног имена", + "groupSearchBase": "База групне претраге", + "adminGroup": "Администраторска група", + "allowedUsers": "Дозвољени корисници" + } +} diff --git a/plugins/ldap/locales/translated/sv_SE.json b/plugins/ldap/locales/translated/sv_SE.json new file mode 100644 index 000000000..f00a2306a --- /dev/null +++ b/plugins/ldap/locales/translated/sv_SE.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "Logga in med {{name}}", + "loading": "Belastning...", + "requiredField": "Detta fält är obligatoriskt", + "username": "LDAP-användarnamn", + "password": "LDAP-lösenord", + "signIn": "Logga in", + "providers": { + "docsLink": "Visa dokument", + "name": "Visningsnamn", + "enabled": "Aktiverad", + "saving": "Sparande...", + "saved": "Sparad", + "saveFailed": "Misslyckades med att spara", + "deleted": "Borttagen", + "deleteFailed": "Misslyckades med att radera", + "cancel": "Avboka" + }, + "fields": { + "host": "LDAP-värd", + "port": "Hamn", + "useTls": "Använd TLS (LDAPS)", + "bindDn": "Bind DN", + "bindPassword": "Bind lösenord", + "userSearchBase": "Användarsökningsbas", + "userSearchFilter": "Användarsökningsfilter", + "usernameAttr": "Användarnamnsattribut", + "displayNameAttr": "Attribut för visningsnamn", + "groupSearchBase": "Gruppsökningsbas", + "adminGroup": "Administratörsgrupp", + "allowedUsers": "Tillåtna användare" + } +} diff --git a/plugins/ldap/locales/translated/th_TH.json b/plugins/ldap/locales/translated/th_TH.json new file mode 100644 index 000000000..2a4b7edb0 --- /dev/null +++ b/plugins/ldap/locales/translated/th_TH.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "เข้าสู่ระบบด้วย {{name}}", + "loading": "กำลังโหลด...", + "requiredField": "ช่องนี้จำเป็นต้องกรอก", + "username": "ชื่อผู้ใช้ LDAP", + "password": "รหัสผ่าน LDAP", + "signIn": "เข้าสู่ระบบ", + "providers": { + "docsLink": "ดูเอกสาร", + "name": "ชื่อที่แสดง", + "enabled": "เปิดใช้งาน", + "saving": "ประหยัด...", + "saved": "บันทึกแล้ว", + "saveFailed": "บันทึกไม่สำเร็จ", + "deleted": "ลบแล้ว", + "deleteFailed": "ลบไม่สำเร็จ", + "cancel": "ยกเลิก" + }, + "fields": { + "host": "โฮสต์ LDAP", + "port": "ท่าเรือ", + "useTls": "ใช้ TLS (LDAPS)", + "bindDn": "ผูก DN", + "bindPassword": "รหัสผ่านผูกมัด", + "userSearchBase": "ฐานการค้นหาผู้ใช้", + "userSearchFilter": "ตัวกรองการค้นหาผู้ใช้", + "usernameAttr": "คุณลักษณะชื่อผู้ใช้", + "displayNameAttr": "คุณลักษณะชื่อที่แสดง", + "groupSearchBase": "ฐานการค้นหากลุ่ม", + "adminGroup": "กลุ่มผู้ดูแลระบบ", + "allowedUsers": "ผู้ใช้ที่ได้รับอนุญาต" + } +} diff --git a/plugins/ldap/locales/translated/tr_TR.json b/plugins/ldap/locales/translated/tr_TR.json new file mode 100644 index 000000000..2402af376 --- /dev/null +++ b/plugins/ldap/locales/translated/tr_TR.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "{{name}} ile giriş yap", + "loading": "Yükleniyor...", + "requiredField": "Bu alan zorunludur", + "username": "LDAP Kullanıcı Adı", + "password": "LDAP Parolası", + "signIn": "Oturum Aç", + "providers": { + "docsLink": "Belgeleri görüntüle", + "name": "Görünen Ad", + "enabled": "Etkin", + "saving": "Kaydediliyor...", + "saved": "Kaydedildi", + "saveFailed": "Kaydedilemedi", + "deleted": "Silindi", + "deleteFailed": "Silinemedi", + "cancel": "İptal" + }, + "fields": { + "host": "LDAP Sunucusu", + "port": "Port", + "useTls": "TLS Kullan (LDAPS)", + "bindDn": "Bind DN", + "bindPassword": "Bind Parolası", + "userSearchBase": "Kullanıcı Arama Tabanı", + "userSearchFilter": "Kullanıcı Arama Süzgeci", + "usernameAttr": "Kullanıcı Adı Özniteliği", + "displayNameAttr": "Görünen Ad Özniteliği", + "groupSearchBase": "Grup Arama Tabanı", + "adminGroup": "Yönetici Grubu", + "allowedUsers": "İzin Verilen Kullanıcılar" + } +} diff --git a/plugins/ldap/locales/translated/uk_UA.json b/plugins/ldap/locales/translated/uk_UA.json new file mode 100644 index 000000000..616f23343 --- /dev/null +++ b/plugins/ldap/locales/translated/uk_UA.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "Увійти за допомогою {{name}}", + "loading": "Завантаження...", + "requiredField": "Це поле обов'язкове", + "username": "Ім'я користувача LDAP", + "password": "Пароль LDAP", + "signIn": "Увійти", + "providers": { + "docsLink": "Переглянути документи", + "name": "Відображуване ім'я", + "enabled": "Увімкнено", + "saving": "Збереження...", + "saved": "Збережено", + "saveFailed": "Не вдалося зберегти", + "deleted": "Видалено", + "deleteFailed": "Не вдалося видалити", + "cancel": "Скасувати" + }, + "fields": { + "host": "Хост LDAP", + "port": "Порт", + "useTls": "Використовувати TLS (LDAPS)", + "bindDn": "Прив'язане DN", + "bindPassword": "Прив’язати пароль", + "userSearchBase": "База пошуку користувачів", + "userSearchFilter": "Фільтр пошуку користувачів", + "usernameAttr": "Атрибут імені користувача", + "displayNameAttr": "Атрибут відображуваного імені", + "groupSearchBase": "База групового пошуку", + "adminGroup": "Група адміністраторів", + "allowedUsers": "Дозволені користувачі" + } +} diff --git a/plugins/ldap/locales/translated/vi_VN.json b/plugins/ldap/locales/translated/vi_VN.json new file mode 100644 index 000000000..38e58069f --- /dev/null +++ b/plugins/ldap/locales/translated/vi_VN.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "Đăng nhập bằng {{name}}", + "loading": "Đang tải...", + "requiredField": "Trường này là bắt buộc", + "username": "Tên người dùng LDAP", + "password": "Mật khẩu LDAP", + "signIn": "Đăng nhập", + "providers": { + "docsLink": "Xem tài liệu", + "name": "Tên hiển thị", + "enabled": "Đã bật", + "saving": "Đang lưu...", + "saved": "Đã lưu", + "saveFailed": "Không thể lưu", + "deleted": "Đã xóa", + "deleteFailed": "Không thể xóa", + "cancel": "Hủy" + }, + "fields": { + "host": "Máy chủ LDAP", + "port": "Cổng", + "useTls": "Dùng TLS (LDAPS)", + "bindDn": "Bind DN", + "bindPassword": "Mật khẩu Bind", + "userSearchBase": "Cơ sở tìm kiếm người dùng", + "userSearchFilter": "Bộ lọc tìm kiếm người dùng", + "usernameAttr": "Thuộc tính tên người dùng", + "displayNameAttr": "Thuộc tính tên hiển thị", + "groupSearchBase": "Cơ sở tìm kiếm nhóm", + "adminGroup": "Nhóm quản trị", + "allowedUsers": "Người dùng được phép" + } +} diff --git a/plugins/ldap/locales/translated/zh_CN.json b/plugins/ldap/locales/translated/zh_CN.json new file mode 100644 index 000000000..a2739e163 --- /dev/null +++ b/plugins/ldap/locales/translated/zh_CN.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "使用{{name}}登录", + "loading": "加载中...", + "requiredField": "此字段为必填项", + "username": "LDAP 用户名", + "password": "LDAP 密码", + "signIn": "登录", + "providers": { + "docsLink": "查看文档", + "name": "显示名称", + "enabled": "已启用", + "saving": "保存中...", + "saved": "已保存", + "saveFailed": "保存失败", + "deleted": "已删除", + "deleteFailed": "删除失败", + "cancel": "取消" + }, + "fields": { + "host": "LDAP 主机", + "port": "端口", + "useTls": "使用 TLS (LDAPS)", + "bindDn": "绑定 DN", + "bindPassword": "绑定密码", + "userSearchBase": "用户搜索基准", + "userSearchFilter": "用户搜索过滤器", + "usernameAttr": "用户名属性", + "displayNameAttr": "显示名称属性", + "groupSearchBase": "群组搜索基准", + "adminGroup": "管理员群组", + "allowedUsers": "允许的用户" + } +} diff --git a/plugins/ldap/locales/translated/zh_TW.json b/plugins/ldap/locales/translated/zh_TW.json new file mode 100644 index 000000000..e7bb9a537 --- /dev/null +++ b/plugins/ldap/locales/translated/zh_TW.json @@ -0,0 +1,33 @@ +{ + "loginWithProvider": "使用 {{name}} 登入", + "loading": "載入中...", + "requiredField": "此欄位為必填", + "username": "LDAP 使用者名稱", + "password": "LDAP 密碼", + "signIn": "登入", + "providers": { + "docsLink": "檢視文件", + "name": "顯示名稱", + "enabled": "已啟用", + "saving": "儲存中...", + "saved": "已儲存", + "saveFailed": "儲存失敗", + "deleted": "已刪除", + "deleteFailed": "刪除失敗", + "cancel": "取消" + }, + "fields": { + "host": "LDAP 主機", + "port": "連接埠", + "useTls": "使用 TLS (LDAPS)", + "bindDn": "綁定 DN", + "bindPassword": "綁定密碼", + "userSearchBase": "使用者搜尋基底", + "userSearchFilter": "使用者搜尋篩選器", + "usernameAttr": "使用者名稱屬性", + "displayNameAttr": "顯示名稱屬性", + "groupSearchBase": "群組搜尋基底", + "adminGroup": "管理員群組", + "allowedUsers": "允許的使用者" + } +} diff --git a/plugins/ldap/manifest.json b/plugins/ldap/manifest.json new file mode 100644 index 000000000..ed92a9fd4 --- /dev/null +++ b/plugins/ldap/manifest.json @@ -0,0 +1,49 @@ +{ + "id": "ldap", + "name": "LDAP", + "version": "1.0.0", + "description": "Sign in with a username and password checked against an LDAP or Active Directory server.", + "author": { + "name": "Termix" + }, + "license": "MIT", + "repository": "https://github.com/Termix-SSH/Termix", + "category": "Access & Security", + "icon": "Server", + "engine": { + "termix": ">=2.9.0", + "api": "1" + }, + "capabilities": [ + "db:own", + "secrets:own", + "network:serve", + "auth:provide", + "ui:surface" + ], + "contributes": { + "auth": { + "loginMethods": ["ldap"] + }, + "permissions": [ + { + "name": "manage", + "titleKey": "permissions.manage.title", + "descriptionKey": "permissions.manage.description", + "defaultRoles": ["admin"] + } + ], + "settings": { + "admin": [ + { + "key": "providers", + "type": "custom", + "component": "providers", + "permission": "manage" + } + ] + } + }, + "locales": "locales", + "platforms": ["linux", "win32", "darwin"] +} diff --git a/plugins/ldap/migrations/mysql/0001_create_providers.sql b/plugins/ldap/migrations/mysql/0001_create_providers.sql new file mode 100644 index 000000000..0acd44d45 --- /dev/null +++ b/plugins/ldap/migrations/mysql/0001_create_providers.sql @@ -0,0 +1,12 @@ +-- ldap 0001: create_providers +-- Generated by termix-plugin migrations. Review before committing. + +CREATE TABLE IF NOT EXISTS `p_ldap_providers` ( + `id` int AUTO_INCREMENT PRIMARY KEY, + `name` text NOT NULL, + `enabled` boolean NOT NULL DEFAULT true, + `display_order` int NOT NULL DEFAULT 0, + `config` text NOT NULL, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP) +); diff --git a/plugins/ldap/migrations/postgres/0001_create_providers.sql b/plugins/ldap/migrations/postgres/0001_create_providers.sql new file mode 100644 index 000000000..d27cd1e10 --- /dev/null +++ b/plugins/ldap/migrations/postgres/0001_create_providers.sql @@ -0,0 +1,12 @@ +-- ldap 0001: create_providers +-- Generated by termix-plugin migrations. Review before committing. + +CREATE TABLE IF NOT EXISTS "p_ldap_providers" ( + "id" serial PRIMARY KEY, + "name" text NOT NULL, + "enabled" boolean NOT NULL DEFAULT true, + "display_order" integer NOT NULL DEFAULT 0, + "config" text NOT NULL, + "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP +); diff --git a/plugins/ldap/migrations/snapshot.json b/plugins/ldap/migrations/snapshot.json new file mode 100644 index 000000000..362b04235 --- /dev/null +++ b/plugins/ldap/migrations/snapshot.json @@ -0,0 +1,43 @@ +{ + "version": 1, + "tables": { + "providers": { + "columns": { + "id": { + "type": "id", + "primaryKey": true, + "notNull": true + }, + "name": { + "type": "text", + "notNull": true + }, + "enabled": { + "type": "boolean", + "notNull": true, + "defaultValue": true + }, + "displayOrder": { + "type": "integer", + "notNull": true, + "defaultValue": 0 + }, + "config": { + "type": "text", + "notNull": true + }, + "createdAt": { + "type": "timestamp", + "notNull": true, + "defaultNow": true + }, + "updatedAt": { + "type": "timestamp", + "notNull": true, + "defaultNow": true + } + }, + "indexes": [] + } + } +} diff --git a/plugins/ldap/migrations/sqlite/0001_create_providers.sql b/plugins/ldap/migrations/sqlite/0001_create_providers.sql new file mode 100644 index 000000000..ae68f949f --- /dev/null +++ b/plugins/ldap/migrations/sqlite/0001_create_providers.sql @@ -0,0 +1,12 @@ +-- ldap 0001: create_providers +-- Generated by termix-plugin migrations. Review before committing. + +CREATE TABLE IF NOT EXISTS "p_ldap_providers" ( + "id" integer PRIMARY KEY AUTOINCREMENT, + "name" text NOT NULL, + "enabled" integer NOT NULL DEFAULT 1, + "display_order" integer NOT NULL DEFAULT 0, + "config" text NOT NULL, + "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP +); diff --git a/plugins/ldap/package.json b/plugins/ldap/package.json new file mode 100644 index 000000000..8b4caed5b --- /dev/null +++ b/plugins/ldap/package.json @@ -0,0 +1,19 @@ +{ + "name": "@termix-plugin/ldap", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "build": "termix-plugin build", + "test": "termix-plugin test", + "typecheck": "tsc -p tsconfig.json", + "validate": "termix-plugin validate" + }, + "dependencies": { + "ldapjs": "^3.0.7" + }, + "devDependencies": { + "@termix/plugin-sdk": "*", + "@types/ldapjs": "^3.0.6" + } +} diff --git a/plugins/ldap/src/backend/index.ts b/plugins/ldap/src/backend/index.ts new file mode 100644 index 000000000..3d3293d5e --- /dev/null +++ b/plugins/ldap/src/backend/index.ts @@ -0,0 +1,34 @@ +import type { Router } from "express"; +import type { PluginContext } from "@termix/plugin-sdk/backend"; +import { providers } from "./tables.js"; +import { createProviderStore } from "./providers.js"; +import { createLdapLogin, METHOD_ID } from "./ldap.js"; +import { registerLdapRoutes } from "./routes.js"; + +export async function activate(ctx: PluginContext) { + const table = await ctx.db.define(providers); + const store = createProviderStore(ctx, table); + + // One form per enabled directory. + ctx.auth.registerLoginMethod({ + id: METHOD_ID, + labelKey: "loginWithLdap", + icon: "server", + kind: "form", + external: true, + describe: async () => + (await store.listRows()).map((row) => ({ + id: String(row.id), + label: row.name, + enabled: !!row.enabled, + type: "ldap", + })), + verify: createLdapLogin(ctx, store), + }); + + registerLdapRoutes(ctx.http.router(), ctx, store); +} + +export async function deactivate() { + // Everything above was registered through ctx and is disposed by core. +} diff --git a/plugins/ldap/src/backend/ldap.ts b/plugins/ldap/src/backend/ldap.ts new file mode 100644 index 000000000..05d6a4822 --- /dev/null +++ b/plugins/ldap/src/backend/ldap.ts @@ -0,0 +1,205 @@ +/** + * LDAP sign-in as a form login method: a service bind to find the user, a + * bind as the user to check the password, then an optional admin group + * lookup. Core finds or provisions the user and issues the session. + */ + +import ldap from "ldapjs"; +import { + LoginMethodError, + type PluginContext, + type PluginLoginRequest, + type PluginVerifiedIdentity, +} from "@termix/plugin-sdk/backend"; +import type { ProviderStore } from "./providers.js"; +import type { LdapConfig } from "./types.js"; + +export const METHOD_ID = "ldap"; + +export function ldapEscapeFilter(value: string): string { + return value.replace( + /[\\*()\x00]/g, + (c) => `\\${c.charCodeAt(0).toString(16).padStart(2, "0")}`, + ); +} + +/** The provider id identities from this directory are stored under. */ +export function identityProvider(providerId: number): string { + return `ldap:${providerId}`; +} + +function createClient(config: LdapConfig): ldap.Client { + const useTLS = !!config.useTLS; + const url = `${useTLS ? "ldaps" : "ldap"}://${config.host}:${config.port || 389}`; + return ldap.createClient({ + url, + tlsOptions: useTLS ? { rejectUnauthorized: false } : undefined, + }); +} + +function bind(client: ldap.Client, dn: string, password: string) { + return new Promise((resolve, reject) => { + client.bind(dn, password, (err) => (err ? reject(err) : resolve())); + }); +} + +function search( + client: ldap.Client, + base: string, + filter: string, + attributes: string[], +): Promise { + return new Promise((resolve, reject) => { + const entries: ldap.SearchEntry[] = []; + client.search(base, { filter, attributes, scope: "sub" }, (err, res) => { + if (err) return reject(err); + res.on("searchEntry", (entry) => entries.push(entry)); + res.on("error", reject); + res.on("end", () => resolve(entries)); + }); + }); +} + +function unbind(client: ldap.Client): void { + try { + client.unbind(); + } catch { + // best effort + } +} + +function firstValue(entry: ldap.SearchEntry, key: string): string { + const attr = entry.attributes.find((a) => a.type === key); + if (!attr) return ""; + return Array.isArray(attr.values) ? attr.values[0] : String(attr.values); +} + +export function createLdapLogin(ctx: PluginContext, store: ProviderStore) { + return async function verify( + request: PluginLoginRequest, + instanceId: string | null, + ): Promise { + const body = request.body ?? {}; + const providerId = Number(instanceId ?? body.providerId); + const username = typeof body.username === "string" ? body.username : ""; + const password = typeof body.password === "string" ? body.password : ""; + + if (!Number.isInteger(providerId) || !username || !password) { + throw new LoginMethodError( + "providerId, username, and password are required", + 400, + ); + } + + // Same limits as password login: an LDAP bind is a password guess. + const clientIp = request.ip || "unknown"; + const rateLimitKey = `${providerId}:${username}`; + const lock = await ctx.auth.loginRateLimit.isLocked(clientIp, rateLimitKey); + if (lock.locked) { + const error = new LoginMethodError( + "Too many login attempts. Please try again later.", + 429, + ); + Object.assign(error, { remainingTime: lock.remainingTime }); + throw error; + } + + const provider = await store.find(providerId); + if (!provider || !provider.enabled) { + throw new LoginMethodError("LDAP provider not found", 404); + } + const config = provider.config; + if ( + !config.host || + !config.bindDN || + !config.userSearchBase || + !config.userSearchFilter + ) { + throw new LoginMethodError("LDAP provider is misconfigured", 500); + } + + const refuse = async (reason: string) => { + await ctx.auth.loginRateLimit.recordFailure(clientIp, rateLimitKey); + ctx.log.warn(`LDAP login refused for ${username}: ${reason}`); + return new LoginMethodError("Invalid username or password", 401); + }; + + const serviceClient = createClient(config); + try { + await bind(serviceClient, config.bindDN, config.bindPassword); + + const filter = config.userSearchFilter.replace( + /\{\{username\}\}/g, + ldapEscapeFilter(username), + ); + const entries = await search( + serviceClient, + config.userSearchBase, + filter, + [ + config.usernameAttribute || "uid", + config.displayNameAttribute || "cn", + "mail", + "email", + ], + ); + if (entries.length === 0) throw await refuse("user not found"); + + const userEntry = entries[0]; + const userDN = userEntry.dn.toString(); + const uid = + firstValue(userEntry, config.usernameAttribute || "uid") || username; + const displayName = + firstValue(userEntry, config.displayNameAttribute || "cn") || username; + const email = + firstValue(userEntry, "mail") || firstValue(userEntry, "email") || ""; + + const userClient = createClient(config); + try { + await bind(userClient, userDN, password); + } catch { + throw await refuse("wrong password"); + } finally { + unbind(userClient); + } + + let isAdmin: boolean | undefined; + if (config.adminGroup && config.groupSearchBase) { + isAdmin = false; + try { + const groups = await search( + serviceClient, + config.groupSearchBase, + `(member=${ldapEscapeFilter(userDN)})`, + ["cn", "dn"], + ); + isAdmin = groups.some( + (group) => + firstValue(group, "cn") === config.adminGroup || + group.dn.toString() === config.adminGroup, + ); + } catch (error) { + ctx.log.warn(`LDAP group check failed: ${String(error)}`); + } + } + + return { + kind: "external", + provider: identityProvider(providerId), + subject: uid, + email: email || null, + name: displayName, + isAdmin, + allowedUsers: config.allowedUsers ?? null, + legacy: { + identifier: `ldap:${providerId}:${uid}`, + providerRowId: providerId, + }, + rememberMe: !!body.rememberMe, + rateLimitKey, + }; + } finally { + unbind(serviceClient); + } + }; +} diff --git a/plugins/ldap/src/backend/providers.ts b/plugins/ldap/src/backend/providers.ts new file mode 100644 index 000000000..1210c5bee --- /dev/null +++ b/plugins/ldap/src/backend/providers.ts @@ -0,0 +1,141 @@ +import { and, asc, desc, eq } from "drizzle-orm"; +import type { PluginContext } from "@termix/plugin-sdk/backend"; +import type { LdapConfig, ProviderRow } from "./types.js"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +// ctx.db.define hands the table back untyped and the drizzle handle is the +// server's own, so both are typed at this module's edge. +type Table = any; +type Drizzle = any; +/* eslint-enable @typescript-eslint/no-explicit-any */ + +const LEGACY_PREFIXES = ["encoded:", "encrypted:"]; + +export type ProviderStore = ReturnType; + +export function createProviderStore(ctx: PluginContext, table: Table) { + const client = () => ctx.db.client(); + + /** Opens the bind password: sealed, or the 2.8 base64 forms. */ + async function openSecret(value: string): Promise { + const prefix = LEGACY_PREFIXES.find((p) => value.startsWith(p)); + if (prefix) { + return Buffer.from(value.slice(prefix.length), "base64").toString("utf8"); + } + return (await ctx.secrets.unseal(value)) ?? value; + } + + async function parseConfig(raw: string): Promise { + let config: Record; + try { + config = JSON.parse(raw); + } catch { + config = {}; + } + if (typeof config.bindPassword === "string" && config.bindPassword) { + config.bindPassword = await openSecret(config.bindPassword); + } + return config as unknown as LdapConfig; + } + + async function sealConfig(config: Record): Promise { + const out = { ...config }; + if (typeof out.bindPassword === "string" && out.bindPassword) { + out.bindPassword = await ctx.secrets.seal(out.bindPassword); + } + return JSON.stringify(out); + } + + async function findRow(id: number): Promise { + const drizzle = await client(); + const rows = (await drizzle + .select() + .from(table) + .where(eq(table.id, id)) + .limit(1)) as ProviderRow[]; + return rows[0] ?? null; + } + + return { + parseConfig, + findRow, + + async listRows(): Promise { + const drizzle = await client(); + return (await drizzle + .select() + .from(table) + .orderBy(asc(table.displayOrder), asc(table.id))) as ProviderRow[]; + }, + + async find(id: number): Promise<{ + row: ProviderRow; + enabled: boolean; + config: LdapConfig; + } | null> { + const row = await findRow(id); + if (!row) return null; + return { + row, + enabled: !!row.enabled, + config: await parseConfig(row.config), + }; + }, + + async create(input: { + name: string; + enabled: boolean; + displayOrder: number; + config: Record; + }): Promise { + const drizzle = await client(); + const now = new Date().toISOString(); + await drizzle.insert(table).values({ + name: input.name, + enabled: input.enabled, + displayOrder: input.displayOrder, + config: await sealConfig(input.config), + createdAt: now, + updatedAt: now, + }); + await ctx.db.persist(); + // No returning() on every engine: find the row by what was written. + const rows = (await drizzle + .select() + .from(table) + .where(and(eq(table.name, input.name), eq(table.createdAt, now))) + .orderBy(desc(table.id)) + .limit(1)) as ProviderRow[]; + return rows[0]; + }, + + async update( + id: number, + values: Partial<{ + name: string; + enabled: boolean; + displayOrder: number; + config: Record; + }>, + ): Promise { + const drizzle = await client(); + const { config, ...rest } = values; + await drizzle + .update(table) + .set({ + ...rest, + ...(config ? { config: await sealConfig(config) } : {}), + updatedAt: new Date().toISOString(), + }) + .where(eq(table.id, id)); + await ctx.db.persist(); + return findRow(id); + }, + + async remove(id: number): Promise { + const drizzle = await client(); + await drizzle.delete(table).where(eq(table.id, id)); + await ctx.db.persist(); + }, + }; +} diff --git a/plugins/ldap/src/backend/routes.ts b/plugins/ldap/src/backend/routes.ts new file mode 100644 index 000000000..2b67570c8 --- /dev/null +++ b/plugins/ldap/src/backend/routes.ts @@ -0,0 +1,265 @@ +import type { Request, Response, Router } from "express"; +import type { PluginContext } from "@termix/plugin-sdk/backend"; +import { identityProvider } from "./ldap.js"; +import type { ProviderStore } from "./providers.js"; +import { REQUIRED_FIELDS, type ProviderRow } from "./types.js"; + +function fail( + ctx: PluginContext, + res: Response, + message: string, + error: unknown, +) { + ctx.log.error( + message, + error instanceof Error ? error : new Error(String(error)), + ); + res.status(500).json({ error: message }); +} + +function missingFields(config: Record): string[] { + return REQUIRED_FIELDS.filter((field) => !config[field]); +} + +export function registerLdapRoutes( + router: Router, + ctx: PluginContext, + store: ProviderStore, +): void { + router.use(ctx.rbac.require("manage") as never); + + async function present(row: ProviderRow) { + const { bindPassword, ...config } = (await store.parseConfig( + row.config, + )) as unknown as Record; + return { + id: row.id, + name: row.name, + enabled: !!row.enabled, + displayOrder: row.displayOrder, + config, + hasBindPassword: typeof bindPassword === "string" && !!bindPassword, + }; + } + + /** + * @openapi + * /plugin-api/ldap/providers: + * get: + * summary: List LDAP directories + * description: Every configured LDAP directory. The bind password is never sent, only whether one is set. Needs ldap.manage. + * tags: + * - LDAP + * responses: + * 200: + * description: Directories. + * 403: + * description: Missing permission. + */ + router.get("/providers", async (_req: Request, res: Response) => { + try { + const rows = await store.listRows(); + res.json({ providers: await Promise.all(rows.map(present)) }); + } catch (error) { + fail(ctx, res, "Failed to list LDAP providers", error); + } + }); + + /** + * @openapi + * /plugin-api/ldap/providers: + * post: + * summary: Add an LDAP directory + * description: Needs ldap.manage. + * tags: + * - LDAP + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * enabled: + * type: boolean + * config: + * type: object + * responses: + * 201: + * description: Directory added. + * 400: + * description: Missing fields. + * 403: + * description: Missing permission. + */ + router.post("/providers", async (req: Request, res: Response) => { + try { + const { + name, + enabled = true, + displayOrder = 0, + config = {}, + } = req.body ?? {}; + if (typeof name !== "string" || !name.trim()) { + res.status(400).json({ error: "Provider name is required" }); + return; + } + const missing = missingFields(config); + if (missing.length > 0) { + res.status(400).json({ + error: `Missing required LDAP fields: ${missing.join(", ")}`, + }); + return; + } + const row = await store.create({ + name: name.trim(), + enabled: !!enabled, + displayOrder: Number(displayOrder) || 0, + config: { ...(config as Record) }, + }); + await ctx.audit.record({ + action: "ldap_provider_create", + resourceType: "ldap_provider", + resourceId: String(row.id), + resourceName: row.name, + success: true, + }); + res.status(201).json(await present(row)); + } catch (error) { + fail(ctx, res, "Failed to create LDAP provider", error); + } + }); + + /** + * @openapi + * /plugin-api/ldap/providers/{id}: + * put: + * summary: Update an LDAP directory + * description: Config fields are merged over the stored ones; an empty bind password keeps the stored one. Needs ldap.manage. + * tags: + * - LDAP + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Directory updated. + * 400: + * description: Missing fields. + * 403: + * description: Missing permission. + * 404: + * description: Directory not found. + */ + router.put("/providers/:id", async (req: Request, res: Response) => { + try { + const id = Number.parseInt(String(req.params.id), 10); + const row = Number.isInteger(id) ? await store.findRow(id) : null; + if (!row) { + res.status(404).json({ error: "Provider not found" }); + return; + } + const { name, enabled, displayOrder, config } = req.body ?? {}; + const values: Parameters[1] = {}; + if (name !== undefined) { + if (typeof name !== "string" || !name.trim()) { + res.status(400).json({ error: "Provider name is required" }); + return; + } + values.name = name.trim(); + } + if (enabled !== undefined) values.enabled = !!enabled; + if (displayOrder !== undefined) { + values.displayOrder = Number(displayOrder) || 0; + } + if (config && typeof config === "object") { + const incoming = { ...(config as Record) }; + if (!incoming.bindPassword) delete incoming.bindPassword; + const merged = { + ...((await store.parseConfig(row.config)) as unknown as Record< + string, + unknown + >), + ...incoming, + }; + const missing = missingFields(merged); + if (missing.length > 0) { + res.status(400).json({ + error: `Missing required LDAP fields: ${missing.join(", ")}`, + }); + return; + } + values.config = merged; + } + const updated = await store.update(id, values); + await ctx.audit.record({ + action: "ldap_provider_update", + resourceType: "ldap_provider", + resourceId: String(id), + resourceName: updated?.name ?? row.name, + success: true, + }); + res.json(await present(updated ?? row)); + } catch (error) { + fail(ctx, res, "Failed to update LDAP provider", error); + } + }); + + /** + * @openapi + * /plugin-api/ldap/providers/{id}: + * delete: + * summary: Delete an LDAP directory + * description: Refused while users still sign in with it. Needs ldap.manage. + * tags: + * - LDAP + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Directory deleted. + * 403: + * description: Missing permission. + * 404: + * description: Directory not found. + * 409: + * description: Users are still linked to it. + */ + router.delete("/providers/:id", async (req: Request, res: Response) => { + try { + const id = Number.parseInt(String(req.params.id), 10); + const row = Number.isInteger(id) ? await store.findRow(id) : null; + if (!row) { + res.status(404).json({ error: "Provider not found" }); + return; + } + if ((await ctx.auth.countLinkedUsers(identityProvider(id))) > 0) { + res.status(409).json({ + error: + "Users still sign in with this directory. Disable it instead, or remove those users first.", + }); + return; + } + await store.remove(id); + await ctx.audit.record({ + action: "ldap_provider_delete", + resourceType: "ldap_provider", + resourceId: String(id), + resourceName: row.name, + success: true, + }); + res.json({ message: "LDAP provider deleted" }); + } catch (error) { + fail(ctx, res, "Failed to delete LDAP provider", error); + } + }); +} diff --git a/plugins/ldap/src/backend/tables.ts b/plugins/ldap/src/backend/tables.ts new file mode 100644 index 000000000..de2293d19 --- /dev/null +++ b/plugins/ldap/src/backend/tables.ts @@ -0,0 +1,24 @@ +import { + boolean, + defineTable, + id, + integer, + text, + timestamp, +} from "@termix/plugin-sdk/db"; + +/** + * LDAP directories. Rows from 2.8 are copied in from sso_providers by core's + * boot migration, keeping their ids, because identities point at them. + */ +export const providers = defineTable("providers", { + id: id(), + name: text().notNull(), + enabled: boolean().notNull().default(true), + displayOrder: integer().notNull().default(0), + config: text().notNull(), + createdAt: timestamp().notNull().defaultNow(), + updatedAt: timestamp().notNull().defaultNow(), +}); + +export const tables = [providers]; diff --git a/plugins/ldap/src/backend/types.ts b/plugins/ldap/src/backend/types.ts new file mode 100644 index 000000000..337fa4c2a --- /dev/null +++ b/plugins/ldap/src/backend/types.ts @@ -0,0 +1,34 @@ +export interface LdapConfig { + host: string; + port: number; + useTLS: boolean; + bindDN: string; + bindPassword: string; + userSearchBase: string; + userSearchFilter: string; + usernameAttribute: string; + displayNameAttribute: string; + groupSearchBase?: string; + adminGroup?: string; + allowedUsers?: string; +} + +export interface ProviderRow { + id: number; + name: string; + enabled: boolean; + displayOrder: number; + config: string; + createdAt: string; + updatedAt: string; +} + +export const REQUIRED_FIELDS = [ + "host", + "port", + "bindDN", + "bindPassword", + "userSearchBase", + "userSearchFilter", + "usernameAttribute", +] as const; diff --git a/plugins/ldap/src/frontend/LdapLoginForms.tsx b/plugins/ldap/src/frontend/LdapLoginForms.tsx new file mode 100644 index 000000000..4811971c4 --- /dev/null +++ b/plugins/ldap/src/frontend/LdapLoginForms.tsx @@ -0,0 +1,101 @@ +import { useEffect, useState } from "react"; +import { ChevronDown, ChevronUp } from "lucide-react"; +import { toast } from "sonner"; +import { Button, Input } from "@termix/plugin-sdk/ui"; +import { + useTranslation, + type LoginMethodUIProps, +} from "@termix/plugin-sdk/frontend"; + +const labelClass = + "text-[10px] font-bold uppercase tracking-widest text-muted-foreground"; + +/** A collapsible username and password form per enabled directory. */ +export function LdapLoginForms({ + instances, + disabled, + submit, +}: LoginMethodUIProps) { + const { t } = useTranslation(); + const [expandedId, setExpandedId] = useState(null); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [loading, setLoading] = useState(false); + + useEffect(() => { + setUsername(""); + setPassword(""); + }, [expandedId]); + + return ( + <> + {instances.map((instance) => { + const expanded = expandedId === instance.id; + return ( +
+ + {expanded && ( +
{ + event.preventDefault(); + if (!username.trim() || !password) { + toast.error(t("requiredField")); + return; + } + setLoading(true); + try { + await submit( + { username: username.trim(), password }, + instance.id, + ); + } finally { + setLoading(false); + } + }} + className="flex flex-col gap-3 p-3 border-t border-border" + > + + + +
+ )} +
+ ); + })} + + ); +} diff --git a/plugins/ldap/src/frontend/ProvidersSetting.tsx b/plugins/ldap/src/frontend/ProvidersSetting.tsx new file mode 100644 index 000000000..61192d7fe --- /dev/null +++ b/plugins/ldap/src/frontend/ProvidersSetting.tsx @@ -0,0 +1,439 @@ +import { useCallback, useEffect, useState, type ReactNode } from "react"; +import { Pencil, Plus, Trash2 } from "lucide-react"; +import { toast } from "sonner"; +import { + Button, + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + Input, + Switch, +} from "@termix/plugin-sdk/ui"; +import { usePluginApi, useTranslation } from "@termix/plugin-sdk/frontend"; +import { createLdapApi, type LdapProvider } from "./ldap-api"; + +type Fields = { + host: string; + port: string; + useTLS: boolean; + bindDN: string; + bindPassword: string; + userSearchBase: string; + userSearchFilter: string; + usernameAttribute: string; + displayNameAttribute: string; + groupSearchBase: string; + adminGroup: string; + allowedUsers: string; +}; + +const EMPTY_FIELDS: Fields = { + host: "", + port: "389", + useTLS: false, + bindDN: "", + bindPassword: "", + userSearchBase: "", + userSearchFilter: "(uid={{username}})", + usernameAttribute: "uid", + displayNameAttribute: "cn", + groupSearchBase: "", + adminGroup: "", + allowedUsers: "", +}; + +function errorMessage(error: unknown, fallback: string): string { + const err = error as { + response?: { data?: { error?: string } }; + message?: string; + }; + return err.response?.data?.error || err.message || fallback; +} + +const labelClass = + "text-[10px] font-semibold text-muted-foreground uppercase tracking-widest"; + +function Field({ + label, + required, + hint, + children, +}: { + label: string; + required?: boolean; + hint?: string; + children: ReactNode; +}) { + return ( +
+ + {hint && ( + {hint} + )} + {children} +
+ ); +} + +function ProviderDialog({ + open, + onOpenChange, + provider, + onSaved, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + provider: LdapProvider | null; + onSaved: () => void; +}) { + const { t } = useTranslation(); + const api = createLdapApi(usePluginApi()); + const [name, setName] = useState(""); + const [enabled, setEnabled] = useState(true); + const [fields, setFields] = useState(EMPTY_FIELDS); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (!open) return; + setName(provider?.name ?? ""); + setEnabled(provider?.enabled ?? true); + const config = provider?.config ?? {}; + const next = { ...EMPTY_FIELDS }; + for (const key of Object.keys(next) as Array) { + const value = config[key]; + if (key === "useTLS") next.useTLS = value === true; + else if (key === "port" && value !== undefined) next.port = String(value); + else if (typeof value === "string") next[key] = value as never; + } + next.bindPassword = ""; + setFields(next); + }, [open, provider]); + + const set = (key: Exclude) => (value: string) => + setFields((prev) => ({ ...prev, [key]: value })); + + async function save() { + if (!name.trim()) { + toast.error(t("providers.nameRequired")); + return; + } + const config: Record = { + host: fields.host.trim(), + port: Number.parseInt(fields.port, 10) || 389, + useTLS: fields.useTLS, + bindDN: fields.bindDN.trim(), + userSearchBase: fields.userSearchBase.trim(), + userSearchFilter: fields.userSearchFilter.trim(), + usernameAttribute: fields.usernameAttribute.trim() || "uid", + displayNameAttribute: fields.displayNameAttribute.trim() || "cn", + groupSearchBase: fields.groupSearchBase.trim() || undefined, + adminGroup: fields.adminGroup.trim() || undefined, + allowedUsers: fields.allowedUsers.trim() || undefined, + }; + if (fields.bindPassword) config.bindPassword = fields.bindPassword; + setSaving(true); + try { + const input = { name: name.trim(), enabled, config }; + if (provider) await api.update(provider.id, input); + else await api.create(input); + toast.success(t("providers.saved")); + onSaved(); + onOpenChange(false); + } catch (error) { + toast.error(errorMessage(error, t("providers.saveFailed"))); + } finally { + setSaving(false); + } + } + + return ( + + + + + {provider ? t("providers.edit") : t("providers.add")} + + +
+ + setName(e.target.value)} + placeholder={t("providers.namePlaceholder")} + className="text-xs" + /> + +
+ + +
+ +
+ + + {t("providers.docsLink")} + + + + set("host")(e.target.value)} + placeholder="ldap.example.com" + className="text-xs" + /> + +
+
+ + set("port")(e.target.value)} + placeholder="389" + className="text-xs" + /> + +
+
+ + setFields((prev) => ({ ...prev, useTLS })) + } + aria-label={t("fields.useTls")} + /> + + {t("fields.useTls")} + +
+
+ + set("bindDN")(e.target.value)} + placeholder="cn=admin,dc=example,dc=com" + className="text-xs" + /> + + + set("bindPassword")(e.target.value)} + className="text-xs" + /> + + + set("userSearchBase")(e.target.value)} + placeholder="ou=users,dc=example,dc=com" + className="text-xs" + /> + + + set("userSearchFilter")(e.target.value)} + placeholder="(uid={{username}})" + className="text-xs" + /> + + + set("usernameAttribute")(e.target.value)} + placeholder="uid" + className="text-xs" + /> + + + set("displayNameAttribute")(e.target.value)} + placeholder="cn" + className="text-xs" + /> + +
+ + set("groupSearchBase")(e.target.value)} + placeholder="ou=groups,dc=example,dc=com" + className="text-xs" + /> + + + set("adminGroup")(e.target.value)} + placeholder="cn=admins,ou=groups,dc=example,dc=com" + className="text-xs" + /> + + + set("allowedUsers")(e.target.value)} + placeholder="user1,user2,@domain.com" + className="text-xs" + /> + +
+
+ +
+ + +
+
+
+
+ ); +} + +/** + * The "providers" custom field on the plugin's admin settings page: the + * directory list with an editor. + */ +export function ProvidersSetting() { + const { t } = useTranslation(); + const api = createLdapApi(usePluginApi()); + const [providers, setProviders] = useState(null); + const [dialogOpen, setDialogOpen] = useState(false); + const [editing, setEditing] = useState(null); + + const load = useCallback(async () => { + try { + setProviders(await api.list()); + } catch (error) { + setProviders([]); + toast.error(errorMessage(error, t("providers.loadFailed"))); + } + // api is rebuilt each render from the same client. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + void load(); + }, [load]); + + async function toggleEnabled(provider: LdapProvider) { + try { + await api.update(provider.id, { enabled: !provider.enabled }); + await load(); + } catch (error) { + toast.error(errorMessage(error, t("providers.saveFailed"))); + } + } + + async function remove(provider: LdapProvider) { + if (!window.confirm(t("providers.deleteConfirm"))) return; + try { + await api.remove(provider.id); + toast.success(t("providers.deleted")); + await load(); + } catch (error) { + toast.error(errorMessage(error, t("providers.deleteFailed"))); + } + } + + if (providers === null) return null; + + return ( +
+ {providers.length === 0 ? ( + + {t("providers.none")} + + ) : ( +
+ {providers.map((provider) => ( +
+ + {provider.name} + + void toggleEnabled(provider)} + aria-label={t("providers.enabled")} + /> + + +
+ ))} +
+ )} + + void load()} + /> +
+ ); +} diff --git a/plugins/ldap/src/frontend/index.tsx b/plugins/ldap/src/frontend/index.tsx new file mode 100644 index 000000000..2ab503901 --- /dev/null +++ b/plugins/ldap/src/frontend/index.tsx @@ -0,0 +1,12 @@ +import type { TermixApp } from "@termix/plugin-sdk/frontend"; +import { LdapLoginForms } from "./LdapLoginForms"; +import { ProvidersSetting } from "./ProvidersSetting"; + +export function activate(app: TermixApp): void { + app.registerLoginMethod({ + id: "ldap", + titleKey: "loginWithLdap", + component: LdapLoginForms, + }); + app.registerSettingsComponent("providers", ProvidersSetting); +} diff --git a/plugins/ldap/src/frontend/ldap-api.ts b/plugins/ldap/src/frontend/ldap-api.ts new file mode 100644 index 000000000..16db12a6d --- /dev/null +++ b/plugins/ldap/src/frontend/ldap-api.ts @@ -0,0 +1,35 @@ +import type { PluginApiClient } from "@termix/plugin-sdk/frontend"; + +export interface LdapProvider { + id: number; + name: string; + enabled: boolean; + displayOrder: number; + config: Record; + hasBindPassword: boolean; +} + +export interface LdapProviderInput { + name?: string; + enabled?: boolean; + config?: Record; +} + +/** Paths are relative to /plugin-api/ldap/. */ +export function createLdapApi(api: PluginApiClient) { + return { + async list() { + return (await api.get<{ providers: LdapProvider[] }>("/providers")).data + .providers; + }, + async create(input: LdapProviderInput) { + return (await api.post("/providers", input)).data; + }, + async update(id: number, input: LdapProviderInput) { + return (await api.put(`/providers/${id}`, input)).data; + }, + async remove(id: number) { + await api.delete(`/providers/${id}`); + }, + }; +} diff --git a/plugins/ldap/tests/backend/helpers.ts b/plugins/ldap/tests/backend/helpers.ts new file mode 100644 index 000000000..1511d2be1 --- /dev/null +++ b/plugins/ldap/tests/backend/helpers.ts @@ -0,0 +1,125 @@ +import http from "node:http"; +import type { AddressInfo } from "node:net"; +import { fileURLToPath } from "node:url"; +import express, { type Router } from "express"; +import { + createMockCtx, + createTestDb, + type MockPluginContext, + type TestDb, +} from "@termix/plugin-sdk/testing"; +import type { PluginManifest } from "@termix/plugin-sdk/manifest"; +import manifestJson from "../../manifest.json"; + +export const pluginDir = fileURLToPath(new URL("../..", import.meta.url)); +export const manifest = manifestJson as unknown as PluginManifest; + +export interface TestServer { + db: TestDb; + mock: MockPluginContext; + request: ( + method: string, + path: string, + options?: { + user?: string; + body?: unknown; + form?: Record; + headers?: Record; + }, + // Route bodies vary per test; asserting on them is the point. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ) => Promise<{ status: number; body: any; location: string | null }>; + close: () => Promise; +} + +const realFetch = globalThis.fetch; + +export async function startServer( + options: { + capabilities?: string[]; + permissions?: string[]; + linkedUsers?: Record; + loginAttemptLimit?: number; + before?: (sqlite: TestDb["sqlite"]) => void; + } = {}, +): Promise { + const db = await createTestDb(pluginDir, { before: options.before }); + db.sqlite + .prepare("INSERT INTO users (id, username, is_admin) VALUES (?, ?, 1)") + .run("admin", "admin"); + + let router: Router | null = null; + const mock = createMockCtx({ + pluginId: manifest.id, + manifest, + capabilities: options.capabilities ?? manifest.capabilities, + db: db.database, + router: () => (router = express.Router()), + permissions: options.permissions, + linkedUsers: options.linkedUsers, + loginAttemptLimit: options.loginAttemptLimit, + }); + + const { activate } = await import("../../src/backend/index.js"); + await activate(mock.ctx); + + const app = express(); + app.use(express.json()); + app.use(express.urlencoded({ extended: false })); + app.use((req, _res, next) => { + mock.setActor(req.header("x-test-user") || undefined); + next(); + }); + app.use((req, res, next) => router!(req, res, next)); + + const server = http.createServer(app); + await new Promise((resolve) => server.listen(0, resolve)); + const { port } = server.address() as AddressInfo; + + return { + db, + mock, + async request( + method, + path, + { user = "admin", body, form, headers = {} } = {}, + ) { + const payload = + form !== undefined + ? new URLSearchParams(form).toString() + : body !== undefined && method !== "GET" + ? JSON.stringify(body) + : undefined; + const response = await realFetch(`http://127.0.0.1:${port}${path}`, { + method, + redirect: "manual", + headers: { + "x-test-user": user, + ...(form !== undefined + ? { "content-type": "application/x-www-form-urlencoded" } + : payload !== undefined + ? { "content-type": "application/json" } + : {}), + ...headers, + }, + body: payload, + }); + const text = await response.text(); + let parsed: unknown = null; + try { + parsed = text ? JSON.parse(text) : null; + } catch { + parsed = text; + } + return { + status: response.status, + body: parsed, + location: response.headers.get("location"), + }; + }, + async close() { + await new Promise((resolve) => server.close(() => resolve())); + db.close(); + }, + }; +} diff --git a/plugins/ldap/tests/backend/ldap.test.ts b/plugins/ldap/tests/backend/ldap.test.ts new file mode 100644 index 000000000..b4f90f7e0 --- /dev/null +++ b/plugins/ldap/tests/backend/ldap.test.ts @@ -0,0 +1,304 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { startServer, type TestServer } from "./helpers.js"; + +const directory = vi.hoisted(() => ({ + users: {} as Record< + string, + { dn: string; password: string; attrs: Record } + >, + adminDns: [] as string[], + servicePassword: "svc", + binds: [] as string[], +})); + +vi.mock("ldapjs", () => { + function createClient() { + return { + bind: (dn: string, password: string, cb: (err?: Error) => void) => { + directory.binds.push(dn); + if (dn === "cn=service" && password === directory.servicePassword) { + return cb(); + } + const user = Object.values(directory.users).find((u) => u.dn === dn); + if (user && user.password === password) return cb(); + cb(new Error("invalid credentials")); + }, + search: ( + base: string, + options: { filter: string }, + cb: (err: Error | null, res: unknown) => void, + ) => { + const listeners: Record void> = {}; + cb(null, { + on: (event: string, fn: (value?: unknown) => void) => { + listeners[event] = fn; + }, + }); + queueMicrotask(() => { + if (base === "ou=groups") { + for (const dn of directory.adminDns) { + if (options.filter.includes(dn)) { + listeners.searchEntry?.({ + dn: { toString: () => "cn=admins,ou=groups" }, + attributes: [{ type: "cn", values: ["admins"] }], + }); + } + } + } else { + const match = /uid=([^)]+)/.exec(options.filter); + const user = match ? directory.users[match[1]] : undefined; + if (user) { + listeners.searchEntry?.({ + dn: { toString: () => user.dn }, + attributes: Object.entries(user.attrs).map(([type, value]) => ({ + type, + values: [value], + })), + }); + } + } + listeners.end?.(); + }); + }, + unbind: () => {}, + }; + } + return { default: { createClient }, createClient }; +}); + +const CONFIG = { + host: "ldap.example", + port: 389, + bindDN: "cn=service", + bindPassword: "svc", + userSearchBase: "ou=people", + userSearchFilter: "(uid={{username}})", + usernameAttribute: "uid", + displayNameAttribute: "cn", + groupSearchBase: "ou=groups", + adminGroup: "admins", + allowedUsers: "@example.com", +}; + +let server: TestServer | null = null; + +beforeEach(() => { + directory.users = { + bob: { + dn: "uid=bob,ou=people", + password: "hunter2", + attrs: { uid: "bob", cn: "Bob Builder", mail: "bob@example.com" }, + }, + }; + directory.adminDns = []; + directory.binds = []; +}); + +afterEach(async () => { + await server?.close(); + server = null; +}); + +async function withDirectory(options: Parameters[0] = {}) { + server = await startServer(options); + const created = await server.request("POST", "/providers", { + body: { name: "Corp LDAP", config: CONFIG }, + }); + expect(created.status).toBe(201); + return { s: server, id: created.body.id as number }; +} + +function verify( + s: TestServer, + instance: string, + body: Record, +) { + const method = s.mock.auth.loginMethods[0]; + return method.verify!( + { body, query: {}, headers: {}, ip: "10.0.0.1" }, + instance, + ); +} + +describe("LDAP sign-in", () => { + it("binds as the service, then as the user, and returns the identity", async () => { + const { s, id } = await withDirectory(); + directory.adminDns = ["uid=bob,ou=people"]; + const identity = await verify(s, String(id), { + username: "bob", + password: "hunter2", + rememberMe: true, + }); + expect(identity).toEqual({ + kind: "external", + provider: `ldap:${id}`, + subject: "bob", + email: "bob@example.com", + name: "Bob Builder", + isAdmin: true, + allowedUsers: "@example.com", + legacy: { identifier: `ldap:${id}:bob`, providerRowId: id }, + rememberMe: true, + rateLimitKey: `${id}:bob`, + }); + expect(directory.binds).toEqual(["cn=service", "uid=bob,ou=people"]); + }); + + it("is not an admin when the directory says so", async () => { + const { s, id } = await withDirectory(); + const identity = await verify(s, String(id), { + username: "bob", + password: "hunter2", + }); + expect(identity).toMatchObject({ isAdmin: false }); + }); + + it("refuses a wrong password and an unknown user the same way", async () => { + const { s, id } = await withDirectory(); + await expect( + verify(s, String(id), { username: "bob", password: "nope" }), + ).rejects.toMatchObject({ + status: 401, + message: "Invalid username or password", + }); + await expect( + verify(s, String(id), { username: "mallory", password: "x" }), + ).rejects.toMatchObject({ + status: 401, + message: "Invalid username or password", + }); + expect(s.mock.auth.loginFailures.get(`10.0.0.1|${id}:bob`)).toBe(1); + }); + + it("locks out after too many failures, like password login", async () => { + const { s, id } = await withDirectory({ loginAttemptLimit: 2 }); + for (let i = 0; i < 2; i++) { + await expect( + verify(s, String(id), { username: "bob", password: "nope" }), + ).rejects.toMatchObject({ status: 401 }); + } + await expect( + verify(s, String(id), { username: "bob", password: "hunter2" }), + ).rejects.toMatchObject({ status: 429 }); + }); + + it("escapes the username in the search filter", async () => { + const { s, id } = await withDirectory(); + await expect( + verify(s, String(id), { username: "bob)(uid=*", password: "hunter2" }), + ).rejects.toMatchObject({ status: 401 }); + }); + + it("refuses a disabled or unknown directory", async () => { + const { s, id } = await withDirectory(); + await s.request("PUT", `/providers/${id}`, { body: { enabled: false } }); + await expect( + verify(s, String(id), { username: "bob", password: "hunter2" }), + ).rejects.toMatchObject({ status: 404 }); + await expect( + verify(s, "999", { username: "bob", password: "hunter2" }), + ).rejects.toMatchObject({ status: 404 }); + expect(await s.mock.auth.loginMethods[0].describe!()).toEqual([ + { id: String(id), label: "Corp LDAP", enabled: false, type: "ldap" }, + ]); + }); + + it("needs a directory, a username and a password", async () => { + const { s } = await withDirectory(); + await expect( + verify(s, null as never, { username: "bob" }), + ).rejects.toMatchObject({ status: 400 }); + }); +}); + +describe("directory admin routes", () => { + it("seal the bind password and never send it back", async () => { + const { s, id } = await withDirectory(); + const stored = s.db.sqlite + .prepare("SELECT config FROM p_ldap_providers WHERE id = ?") + .get(id) as { config: string }; + expect(JSON.parse(stored.config).bindPassword).toMatch(/^sealed:/); + + const listed = await s.request("GET", "/providers"); + expect(listed.body.providers[0]).toMatchObject({ + name: "Corp LDAP", + hasBindPassword: true, + }); + expect(listed.body.providers[0].config.bindPassword).toBeUndefined(); + }); + + it("keep the bind password when an edit leaves it empty", async () => { + const { s, id } = await withDirectory(); + const updated = await s.request("PUT", `/providers/${id}`, { + body: { config: { host: "ldap2.example", bindPassword: "" } }, + }); + expect(updated.body).toMatchObject({ + hasBindPassword: true, + config: { host: "ldap2.example" }, + }); + await expect( + verify(s, String(id), { username: "bob", password: "hunter2" }), + ).resolves.toMatchObject({ subject: "bob" }); + }); + + it("reject a directory with missing fields", async () => { + server = await startServer(); + const response = await server.request("POST", "/providers", { + body: { name: "X", config: { host: "a" } }, + }); + expect(response.status).toBe(400); + }); + + it("will not delete a directory people still sign in with", async () => { + const { s, id } = await withDirectory({ + linkedUsers: { "ldap:1": 1 }, + }); + expect(id).toBe(1); + expect((await s.request("DELETE", `/providers/${id}`)).status).toBe(409); + }); + + it("delete a directory nobody uses", async () => { + const { s, id } = await withDirectory(); + expect((await s.request("DELETE", `/providers/${id}`)).status).toBe(200); + expect(await s.mock.auth.loginMethods[0].describe!()).toEqual([]); + }); + + it("need ldap.manage", async () => { + server = await startServer({ permissions: [] }); + for (const [method, path] of [ + ["GET", "/providers"], + ["POST", "/providers"], + ["PUT", "/providers/1"], + ["DELETE", "/providers/1"], + ]) { + expect((await server.request(method, path, { body: {} })).status).toBe( + 403, + ); + } + }); +}); + +describe("capabilities", () => { + it.each(["db:own", "auth:provide", "network:serve"])( + "activate fails closed without %s", + async (capability) => { + const { manifest } = await import("./helpers.js"); + await expect( + startServer({ + capabilities: manifest.capabilities.filter((c) => c !== capability), + }), + ).rejects.toThrow(); + }, + ); + + it("cannot read a bind password without secrets:own", async () => { + const { manifest } = await import("./helpers.js"); + server = await startServer({ + capabilities: manifest.capabilities.filter((c) => c !== "secrets:own"), + }); + const response = await server.request("POST", "/providers", { + body: { name: "Corp LDAP", config: CONFIG }, + }); + expect(response.status).toBe(500); + }); +}); diff --git a/plugins/ldap/tests/frontend/activate.test.tsx b/plugins/ldap/tests/frontend/activate.test.tsx new file mode 100644 index 000000000..0e26f4ef9 --- /dev/null +++ b/plugins/ldap/tests/frontend/activate.test.tsx @@ -0,0 +1,93 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { fireEvent, screen, waitFor } from "@testing-library/react"; +import { + renderWithApp, + type RenderedPluginApp, +} from "@termix/plugin-sdk/testing"; +import type { PluginManifest } from "@termix/plugin-sdk/manifest"; +import * as plugin from "../../src/frontend/index"; +import manifestJson from "../../manifest.json"; +import locales from "../../locales/en.json"; + +const manifest = manifestJson as unknown as PluginManifest; + +let rendered: RenderedPluginApp | null = null; + +afterEach(async () => { + await rendered?.deactivate(); + rendered = null; +}); + +describe(`${manifest.id} activate`, () => { + it("registers the login form and the directory settings", async () => { + rendered = await renderWithApp(plugin, { manifest, locales }); + expect(rendered.registered.loginMethods()).toEqual(["ldap"]); + expect(rendered.registered.settingsComponents()).toEqual(["providers"]); + }); + + it("submits the username and password for the chosen directory", async () => { + rendered = await renderWithApp(plugin, { manifest, locales }); + const submit = vi.fn(async () => {}); + rendered.renderLoginMethod("ldap", { + instances: [{ id: "4", label: "Corp" }], + submit, + }); + fireEvent.click(screen.getByText("Login with Corp")); + fireEvent.change(screen.getByLabelText(locales.username), { + target: { value: " bob " }, + }); + fireEvent.change(screen.getByLabelText(locales.password), { + target: { value: "hunter2" }, + }); + fireEvent.click(screen.getByText(locales.signIn)); + await waitFor(() => + expect(submit).toHaveBeenCalledWith( + { username: "bob", password: "hunter2" }, + "4", + ), + ); + }); + + it("does not submit an empty form", async () => { + rendered = await renderWithApp(plugin, { manifest, locales }); + const submit = vi.fn(async () => {}); + rendered.renderLoginMethod("ldap", { + instances: [{ id: "4", label: "Corp" }], + submit, + }); + fireEvent.click(screen.getByText("Login with Corp")); + fireEvent.click(screen.getByText(locales.signIn)); + expect(submit).not.toHaveBeenCalled(); + }); + + it("lists directories without their bind password", async () => { + const get = vi.fn(async () => ({ + data: { + providers: [ + { + id: 4, + name: "Corp LDAP", + enabled: true, + displayOrder: 0, + config: { host: "ldap.example" }, + hasBindPassword: true, + }, + ], + }, + })); + rendered = await renderWithApp(plugin, { + manifest, + locales, + api: { get } as never, + }); + rendered.renderSettingsComponent("providers"); + expect(await screen.findByText("Corp LDAP")).toBeTruthy(); + expect(get).toHaveBeenCalledWith("/providers"); + }); + + it("removes everything on deactivate", async () => { + const app = await renderWithApp(plugin, { manifest, locales }); + await app.deactivate(); + expect(app.registered.loginMethods()).toEqual([]); + }); +}); diff --git a/plugins/ldap/tsconfig.json b/plugins/ldap/tsconfig.json new file mode 100644 index 000000000..7b35314c3 --- /dev/null +++ b/plugins/ldap/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@termix/plugin-sdk/tsconfig.plugin.json", + "include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"] +} diff --git a/plugins/ldap/vitest.config.ts b/plugins/ldap/vitest.config.ts new file mode 100644 index 000000000..31ba004fc --- /dev/null +++ b/plugins/ldap/vitest.config.ts @@ -0,0 +1,3 @@ +import { pluginVitestConfig } from "@termix/plugin-sdk/vitest-preset"; + +export default pluginVitestConfig(import.meta.url); diff --git a/plugins/sso/locales/en.json b/plugins/sso/locales/en.json new file mode 100644 index 000000000..0cfe5658d --- /dev/null +++ b/plugins/sso/locales/en.json @@ -0,0 +1,59 @@ +{ + "loginWithSso": "Sign in with SSO", + "loginWithProvider": "Login with {{name}}", + "loading": "Loading...", + "permissions": { + "manage": { + "title": "Manage SSO providers", + "description": "Add, change and remove the SSO providers people can sign in with." + } + }, + "providers": { + "docsLink": "View docs", + "none": "No SSO providers configured.", + "add": "Add Provider", + "edit": "Edit Provider", + "delete": "Delete Provider", + "deleteConfirm": "Delete this provider? Associated users will be unable to login.", + "name": "Display Name", + "namePlaceholder": "My SSO provider", + "nameRequired": "A display name is required", + "type": "Provider Type", + "enabled": "Enabled", + "save": "Save Provider", + "saving": "Saving...", + "saved": "Saved", + "saveFailed": "Failed to save", + "deleted": "Deleted", + "deleteFailed": "Failed to delete", + "loadFailed": "Failed to load SSO providers", + "cancel": "Cancel", + "copy": "Copy", + "copied": "Copied", + "redirectUri": "Redirect URI", + "redirectUriDesc": "Register this URL as the redirect or callback URL in your identity provider.", + "legacyCallback": "Keep the old redirect URI", + "legacyCallbackDesc": "This provider was set up before Termix 2.9 and still uses the old redirect URI, which keeps working. To move to the new one, add {{uri}} in your identity provider first, then turn this off.", + "authorizationUrl": "Authorization URL: {{url}}" + }, + "fields": { + "clientId": "Client ID", + "clientSecret": "Client Secret", + "secretKeep": "A secret is saved. Leave this empty to keep it.", + "issuerUrl": "Issuer URL", + "authUrl": "Authorization URL", + "tokenUrl": "Token URL", + "userIdentifier": "User Identifier Path", + "displayName": "Display Name Path", + "scopes": "Scopes", + "userinfoUrl": "Override Userinfo URL", + "groupClaim": "Group Claim", + "groupClaimDesc": "Optional. The claim path that contains the user's groups. Defaults to groups, roles, then group. Use this for providers with a custom claim (e.g. Zitadel).", + "allowedUsers": "Allowed Users", + "allowedUsersDesc": "One email per line. Leave empty to allow all.", + "adminGroup": "Admin Group", + "adminGroupDesc": "Users in this group are granted admin. Leave empty to disable group sync.", + "caCert": "Custom CA Certificate", + "caCertDesc": "Optional. PEM-encoded CA certificate for OIDC providers using a private or self-signed CA. Leave empty to use the system trust store." + } +} diff --git a/plugins/sso/locales/translated/af_ZA.json b/plugins/sso/locales/translated/af_ZA.json new file mode 100644 index 000000000..2bd784f02 --- /dev/null +++ b/plugins/sso/locales/translated/af_ZA.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "Teken aan met {{name}}", + "loading": "Laai tans...", + "providers": { + "docsLink": "Bekyk dokumente", + "none": "Geen SSO-verskaffers gekonfigureer nie.", + "add": "Voeg Verskaffer by", + "edit": "Wysig Verskaffer", + "delete": "Vee Verskaffer uit", + "deleteConfirm": "Vee hierdie verskaffer uit? Geassosieerde gebruikers sal nie kan aanmeld nie.", + "name": "Vertoonnaam", + "type": "Verskaffertipe", + "enabled": "Geaktiveer", + "save": "Stoor Verskaffer", + "saving": "Stoor...", + "saved": "Gestoor", + "saveFailed": "Kon nie stoor nie", + "deleted": "Verwyder", + "deleteFailed": "Kon nie verwyder nie", + "cancel": "Kanselleer" + }, + "fields": { + "clientId": "Kliënt-ID", + "clientSecret": "Kliëntgeheim", + "issuerUrl": "Uitreiker-URL", + "authUrl": "Magtigings-URL", + "tokenUrl": "Teken-URL", + "userIdentifier": "Gebruikersidentifiseerderpad", + "displayName": "Vertoonnaampad", + "scopes": "Omvangsgebiede", + "userinfoUrl": "Oorskryf Gebruikersinligting URL", + "groupClaim": "Groep Eis", + "groupClaimDesc": "Opsioneel. Die eispad wat die gebruiker se groepe bevat. Standaard is groepe, rolle, dan groep. Gebruik dit vir verskaffers met 'n persoonlike eis (bv. Zitadel).", + "allowedUsers": "Toegelate Gebruikers", + "allowedUsersDesc": "Een e-pos per reël. Los leeg om alles toe te laat.", + "adminGroup": "Admingroep", + "adminGroupDesc": "Gebruikers in hierdie groep kry administrasie. Los leeg om groepsinchronisasie te deaktiveer.", + "caCert": "Pasgemaakte CA-sertifikaat", + "caCertDesc": "Opsioneel. PEM-geënkodeerde CA-sertifikaat vir OIDC-verskaffers wat 'n private of selfgetekende CA gebruik. Los leeg om die stelselvertrouensberging te gebruik." + } +} diff --git a/plugins/sso/locales/translated/ar_SA.json b/plugins/sso/locales/translated/ar_SA.json new file mode 100644 index 000000000..8ea78a4dc --- /dev/null +++ b/plugins/sso/locales/translated/ar_SA.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "تسجيل الدخول باستخدام {{name}}", + "loading": "جارٍ التحميل...", + "providers": { + "docsLink": "عرض المستندات", + "none": "لم يتم تكوين أي مزود SSO.", + "add": "إضافة مزود", + "edit": "تعديل المزود", + "delete": "حذف المزود", + "deleteConfirm": "حذف هذا المزود؟ لن يتمكن المستخدمون المرتبطون من تسجيل الدخول.", + "name": "الاسم المعروض", + "type": "نوع المزود", + "enabled": "مُمكّن", + "save": "حفظ المزود", + "saving": "جارٍ الحفظ...", + "saved": "محفوظ", + "saveFailed": "فشل في الحفظ", + "deleted": "محذوف", + "deleteFailed": "فشل في الحذف", + "cancel": "إلغاء" + }, + "fields": { + "clientId": "معرف العميل", + "clientSecret": "سر العميل", + "issuerUrl": "رابط المُصدر", + "authUrl": "رابط التفويض", + "tokenUrl": "رابط الرمز المميز", + "userIdentifier": "مسار معرّف المستخدم", + "displayName": "مسار اسم العرض", + "scopes": "النطاقات", + "userinfoUrl": "تجاوز رابط Userinfo", + "groupClaim": "مطالبة المجموعة", + "groupClaimDesc": "اختياري. مسار المطالبة الذي يحتوي على مجموعات المستخدم. القيمة الافتراضية هي groups، ثم roles، ثم group. استخدم هذا للموفّرين الذين لديهم مطالبة مخصصة (مثل Zitadel).", + "allowedUsers": "المستخدمون المسموح لهم", + "allowedUsersDesc": "بريد إلكتروني واحد في كل سطر. اتركه فارغًا للسماح للجميع.", + "adminGroup": "مجموعة المسؤولين", + "adminGroupDesc": "يُمنح المستخدمون في هذه المجموعة صلاحيات المسؤول. اتركه فارغًا لتعطيل مزامنة المجموعة.", + "caCert": "شهادة CA مخصصة", + "caCertDesc": "اختياري. شهادة CA بترميز PEM لموفّري OIDC الذين يستخدمون شهادة CA خاصة أو موقّعة ذاتيًا. اتركه فارغًا لاستخدام مخزن الثقة في النظام." + } +} diff --git a/plugins/sso/locales/translated/bg_BG.json b/plugins/sso/locales/translated/bg_BG.json new file mode 100644 index 000000000..e75f61987 --- /dev/null +++ b/plugins/sso/locales/translated/bg_BG.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "Влезте с {{name}}", + "loading": "Зареждане...", + "providers": { + "docsLink": "Преглед на документи", + "none": "Няма конфигурирани доставчици на SSO.", + "add": "Добавяне на доставчик", + "edit": "Редактиране на доставчик", + "delete": "Изтриване на доставчик", + "deleteConfirm": "Да се изтрие ли този доставчик? Свързаните потребители няма да могат да влязат.", + "name": "Показвано име", + "type": "Тип доставчик", + "enabled": "Активирано", + "save": "Запазване на доставчика", + "saving": "Запазване...", + "saved": "Запазено", + "saveFailed": "Запазването не бе успешно", + "deleted": "Изтрито", + "deleteFailed": "Изтриването не бе успешно", + "cancel": "Отказ" + }, + "fields": { + "clientId": "Идентификационен номер на клиента", + "clientSecret": "Клиентска тайна", + "issuerUrl": "URL адрес на издателя", + "authUrl": "URL адрес за оторизация", + "tokenUrl": "URL адрес на токена", + "userIdentifier": "Път на потребителския идентификатор", + "displayName": "Път на показваното име", + "scopes": "Обхвати", + "userinfoUrl": "Замяна на URL адреса на потребителската информация", + "groupClaim": "Групово искане", + "groupClaimDesc": "Незадължително. Пътят на твърдението, който съдържа групите на потребителя. По подразбиране е „групи“, „роли“, „група“. Използвайте това за доставчици с персонализирано твърдение (напр. Zitadel).", + "allowedUsers": "Разрешени потребители", + "allowedUsersDesc": "По един имейл на ред. Оставете празно, за да разрешите всички.", + "adminGroup": "Административна група", + "adminGroupDesc": "Потребителите в тази група са с администраторски права. Оставете празно, за да деактивирате синхронизирането на групата.", + "caCert": "Персонализиран CA сертификат", + "caCertDesc": "Незадължително. PEM-кодиран CA сертификат за OIDC доставчици, използващи частен или самоподписан CA. Оставете празно, за да използвате хранилището за доверени сертификати на системата." + } +} diff --git a/plugins/sso/locales/translated/bn_BD.json b/plugins/sso/locales/translated/bn_BD.json new file mode 100644 index 000000000..e63fa4795 --- /dev/null +++ b/plugins/sso/locales/translated/bn_BD.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "{{name}} দিয়ে লগইন করুন", + "loading": "লোড হচ্ছে...", + "providers": { + "docsLink": "নথি দেখুন", + "none": "কোনো SSO প্রদানকারী কনফিগার করা নেই।", + "add": "প্রদানকারী যোগ করুন", + "edit": "সম্পাদনা প্রদানকারী", + "delete": "প্রোভাইডার মুছুন", + "deleteConfirm": "এই প্রোভাইডারটি মুছে ফেললে সংশ্লিষ্ট ব্যবহারকারীরা লগইন করতে পারবেন না।", + "name": "প্রদর্শনের নাম", + "type": "প্রদানকারীর ধরণ", + "enabled": "সক্ষম করা হয়েছে", + "save": "সরবরাহকারীকে সংরক্ষণ করুন", + "saving": "সঞ্চয়...", + "saved": "সংরক্ষিত", + "saveFailed": "সংরক্ষণ করতে ব্যর্থ হয়েছে", + "deleted": "মুছে ফেলা হয়েছে", + "deleteFailed": "মুছে ফেলতে ব্যর্থ হয়েছে", + "cancel": "বাতিল করুন" + }, + "fields": { + "clientId": "ক্লায়েন্ট আইডি", + "clientSecret": "ক্লায়েন্ট গোপনীয়", + "issuerUrl": "ইস্যুকারী ইউআরএল", + "authUrl": "অনুমোদন ইউআরএল", + "tokenUrl": "টোকেন ইউআরএল", + "userIdentifier": "ব্যবহারকারী শনাক্তকারী পথ", + "displayName": "ডিসপ্লে নেম পাথ", + "scopes": "স্কোপ", + "userinfoUrl": "ব্যবহারকারীর তথ্য ইউআরএল ওভাররাইড করুন", + "groupClaim": "গোষ্ঠী দাবি", + "groupClaimDesc": "ঐচ্ছিক। এটি হলো সেই ক্লেইম পাথ যেখানে ব্যবহারকারীর গ্রুপগুলো থাকে। ডিফল্ট হিসেবে প্রথমে গ্রুপ, তারপর রোল, এবং সবশেষে গ্রুপ থাকে। কাস্টম ক্লেইম আছে এমন প্রোভাইডারদের (যেমন জিটাডেল) জন্য এটি ব্যবহার করুন।", + "allowedUsers": "অনুমোদিত ব্যবহারকারী", + "allowedUsersDesc": "প্রতি লাইনে একটি ইমেল। সবগুলো দেখানোর জন্য খালি রাখুন।", + "adminGroup": "অ্যাডমিন গ্রুপ", + "adminGroupDesc": "এই গ্রুপের ব্যবহারকারীদের অ্যাডমিন অ্যাক্সেস দেওয়া হয়েছে। গ্রুপ সিঙ্ক নিষ্ক্রিয় করতে এটি খালি রাখুন।", + "caCert": "কাস্টম সিএ সার্টিফিকেট", + "caCertDesc": "ঐচ্ছিক। প্রাইভেট বা সেলফ-সাইন্ড CA ব্যবহারকারী OIDC প্রোভাইডারদের জন্য PEM-এনকোডেড CA সার্টিফিকেট। সিস্টেম ট্রাস্ট স্টোর ব্যবহার করতে এটি খালি রাখুন।" + } +} diff --git a/plugins/sso/locales/translated/ca_ES.json b/plugins/sso/locales/translated/ca_ES.json new file mode 100644 index 000000000..8dfe654c9 --- /dev/null +++ b/plugins/sso/locales/translated/ca_ES.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "Inicia la sessió amb {{name}}", + "loading": "S'està carregant...", + "providers": { + "docsLink": "Veure documents", + "none": "No hi ha cap proveïdor d'SSO configurat.", + "add": "Afegeix un proveïdor", + "edit": "Edita el proveïdor", + "delete": "Suprimeix el proveïdor", + "deleteConfirm": "Voleu suprimir aquest proveïdor? Els usuaris associats no podran iniciar la sessió.", + "name": "Nom de visualització", + "type": "Tipus de proveïdor", + "enabled": "Activat", + "save": "Desa el proveïdor", + "saving": "Desant...", + "saved": "Desat", + "saveFailed": "No s'ha pogut desar", + "deleted": "Suprimit", + "deleteFailed": "No s'ha pogut suprimir", + "cancel": "Cancel·la" + }, + "fields": { + "clientId": "ID de client", + "clientSecret": "Secret del client", + "issuerUrl": "URL de l'emissor", + "authUrl": "URL d'autorització", + "tokenUrl": "URL del testimoni", + "userIdentifier": "Ruta d'identificació d'usuari", + "displayName": "Camí del nom de visualització", + "scopes": "Àmbits", + "userinfoUrl": "Substitueix l'URL d'informació de l'usuari", + "groupClaim": "Reclamació de grup", + "groupClaimDesc": "Opcional. La ruta de reclamació que conté els grups de l'usuari. Per defecte, s'utilitzen grups, rols i, a continuació, grup. Feu servir això per a proveïdors amb una reclamació personalitzada (per exemple, Zitadel).", + "allowedUsers": "Usuaris permesos", + "allowedUsersDesc": "Un correu electrònic per línia. Deixeu-ho en blanc per permetre-ho tot.", + "adminGroup": "Grup d'administradors", + "adminGroupDesc": "Els usuaris d'aquest grup tenen dret d'administrador. Deixeu-ho en blanc per desactivar la sincronització del grup.", + "caCert": "Certificat CA personalitzat", + "caCertDesc": "Opcional. Certificat CA codificat amb PEM per a proveïdors OIDC que utilitzen una CA privada o autosignada. Deixeu-ho en blanc per utilitzar el magatzem de confiança del sistema." + } +} diff --git a/plugins/sso/locales/translated/cs_CZ.json b/plugins/sso/locales/translated/cs_CZ.json new file mode 100644 index 000000000..ea27acedb --- /dev/null +++ b/plugins/sso/locales/translated/cs_CZ.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "Přihlaste se pomocí {{name}}", + "loading": "Načítání...", + "providers": { + "docsLink": "Zobrazit dokumenty", + "none": "Nejsou nakonfigurováni žádní poskytovatelé jednotného přihlašování (SSO).", + "add": "Přidat poskytovatele", + "edit": "Upravit poskytovatele", + "delete": "Smazat poskytovatele", + "deleteConfirm": "Smazat tohoto poskytovatele? Přidružení uživatelé se nebudou moci přihlásit.", + "name": "Zobrazovaný název", + "type": "Typ poskytovatele", + "enabled": "Povoleno", + "save": "Uložit poskytovatele", + "saving": "Ukládání...", + "saved": "Uloženo", + "saveFailed": "Uložení se nezdařilo", + "deleted": "Smazáno", + "deleteFailed": "Nepodařilo se smazat", + "cancel": "Zrušit" + }, + "fields": { + "clientId": "ID klienta", + "clientSecret": "Tajemství klienta", + "issuerUrl": "URL vydavatele", + "authUrl": "Autorizační URL", + "tokenUrl": "URL tokenu", + "userIdentifier": "Cesta k identifikátoru uživatele", + "displayName": "Cesta k zobrazovanému názvu", + "scopes": "Rozsahy", + "userinfoUrl": "Přepsat URL s uživatelskými informacemi", + "groupClaim": "Skupinový nárok", + "groupClaimDesc": "Volitelné. Cesta k deklaraci identity, která obsahuje skupiny uživatele. Výchozí nastavení je skupiny, role a poté skupina. Použijte pro poskytovatele s vlastní deklarací identity (např. Zitadel).", + "allowedUsers": "Povolení uživatelé", + "allowedUsersDesc": "Jeden e-mail na řádek. Pro povolení všech ponechte prázdné.", + "adminGroup": "Skupina administrátorů", + "adminGroupDesc": "Uživatelé v této skupině mají oprávnění k administraci. Pro deaktivaci synchronizace skupiny ponechte pole prázdné.", + "caCert": "Vlastní certifikát certifikační autority", + "caCertDesc": "Volitelné. Certifikát certifikační autority s kódováním PEM pro poskytovatele OIDC používající soukromou nebo samopodepsanou certifikační autoritu. Pro použití systémového úložiště důvěryhodných certifikátů ponechte pole prázdné." + } +} diff --git a/plugins/sso/locales/translated/da_DK.json b/plugins/sso/locales/translated/da_DK.json new file mode 100644 index 000000000..29ab0c330 --- /dev/null +++ b/plugins/sso/locales/translated/da_DK.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "Log ind med {{name}}", + "loading": "Indlæser...", + "providers": { + "docsLink": "Se dokumenter", + "none": "Ingen SSO-udbydere er konfigureret.", + "add": "Tilføj udbyder", + "edit": "Rediger udbyder", + "delete": "Slet udbyder", + "deleteConfirm": "Vil du slette denne udbyder? Tilknyttede brugere vil ikke kunne logge ind.", + "name": "Vist navn", + "type": "Udbydertype", + "enabled": "Aktiveret", + "save": "Gem udbyder", + "saving": "Gemmer...", + "saved": "Gemt", + "saveFailed": "Kunne ikke gemme", + "deleted": "Slettet", + "deleteFailed": "Kunne ikke slette", + "cancel": "Ophæve" + }, + "fields": { + "clientId": "Klient-ID", + "clientSecret": "Klienthemmelighed", + "issuerUrl": "Udsteders URL", + "authUrl": "Autorisations-URL", + "tokenUrl": "Token-URL", + "userIdentifier": "Brugeridentifikatorsti", + "displayName": "Sti til visningsnavn", + "scopes": "Omfang", + "userinfoUrl": "Tilsidesæt brugerinfo-URL", + "groupClaim": "Gruppekrav", + "groupClaimDesc": "Valgfrit. Kravsstien, der indeholder brugerens grupper. Standardindstillingen er grupper, roller og derefter gruppe. Brug dette til udbydere med et brugerdefineret krav (f.eks. Zitadel).", + "allowedUsers": "Tilladte brugere", + "allowedUsersDesc": "Én e-mail pr. linje. Lad feltet stå tomt for at tillade alle.", + "adminGroup": "Admingruppe", + "adminGroupDesc": "Brugere i denne gruppe har administratorrettigheder. Lad feltet stå tomt for at deaktivere gruppesynkronisering.", + "caCert": "Brugerdefineret CA-certifikat", + "caCertDesc": "Valgfrit. PEM-kodet CA-certifikat til OIDC-udbydere, der bruger en privat eller selvsigneret CA. Lad feltet stå tomt for at bruge systemets tillidslager." + } +} diff --git a/plugins/sso/locales/translated/de_DE.json b/plugins/sso/locales/translated/de_DE.json new file mode 100644 index 000000000..42846ef7e --- /dev/null +++ b/plugins/sso/locales/translated/de_DE.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "Anmeldung mit {{name}}", + "loading": "Laden...", + "providers": { + "docsLink": "Dokumentation anzeigen", + "none": "Keine SSO-Anbieter konfiguriert.", + "add": "Anbieter hinzufügen", + "edit": "Anbieter bearbeiten", + "delete": "Anbieter löschen", + "deleteConfirm": "Diesen Anbieter löschen? Zugehörige Benutzer können sich nicht mehr anmelden.", + "name": "Anzeigename", + "type": "Anbietertyp", + "enabled": "Aktiviert", + "save": "Anbieter speichern", + "saving": "Speichern...", + "saved": "Gespeichert", + "saveFailed": "Speichern fehlgeschlagen", + "deleted": "Gelöscht", + "deleteFailed": "Löschen fehlgeschlagen", + "cancel": "Abbrechen" + }, + "fields": { + "clientId": "Client-ID", + "clientSecret": "Client-Secret", + "issuerUrl": "Issuer-URL", + "authUrl": "Autorisierungs-URL", + "tokenUrl": "Token-URL", + "userIdentifier": "Benutzerkennungspfad", + "displayName": "Anzeigename-Pfad", + "scopes": "Scopes", + "userinfoUrl": "Userinfo-URL überschreiben", + "groupClaim": "Gruppen-Claim", + "groupClaimDesc": "Optional. Der Claim-Pfad, der die Gruppen des Benutzers enthält. Standardmäßig groups, roles, dann group. Verwenden Sie dies für Anbieter mit einem benutzerdefinierten Claim (z. B. Zitadel).", + "allowedUsers": "Zugelassene Benutzer", + "allowedUsersDesc": "Eine E-Mail pro Zeile. Leer lassen, um alle zuzulassen.", + "adminGroup": "Admin-Gruppe", + "adminGroupDesc": "Benutzer in dieser Gruppe erhalten Admin-Berechtigungen. Leer lassen, um die Gruppensynchronisierung zu deaktivieren.", + "caCert": "Benutzerdefiniertes CA-Zertifikat", + "caCertDesc": "Optional. PEM-kodiertes CA-Zertifikat für OIDC-Anbieter, die eine private oder selbstsignierte Zertifizierungsstelle verwenden. Leer lassen, um den System-Vertrauensspeicher zu verwenden." + } +} diff --git a/plugins/sso/locales/translated/el_GR.json b/plugins/sso/locales/translated/el_GR.json new file mode 100644 index 000000000..4a7681d6b --- /dev/null +++ b/plugins/sso/locales/translated/el_GR.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "Σύνδεση με {{name}}", + "loading": "Φόρτωση...", + "providers": { + "docsLink": "Προβολή εγγράφων", + "none": "Δεν έχουν ρυθμιστεί πάροχοι SSO.", + "add": "Προσθήκη παρόχου", + "edit": "Επεξεργασία παρόχου", + "delete": "Διαγραφή παρόχου", + "deleteConfirm": "Διαγραφή αυτού του παρόχου; Οι συνδεδεμένοι χρήστες δεν θα μπορούν να συνδεθούν.", + "name": "Εμφανιζόμενο όνομα", + "type": "Τύπος παρόχου", + "enabled": "Ενεργοποιημένο", + "save": "Αποθήκευση παρόχου", + "saving": "Οικονομία...", + "saved": "Αποθηκεύτηκε", + "saveFailed": "Η αποθήκευση απέτυχε", + "deleted": "Διαγράφηκε", + "deleteFailed": "Η διαγραφή απέτυχε", + "cancel": "Ματαίωση" + }, + "fields": { + "clientId": "Αναγνωριστικό πελάτη", + "clientSecret": "Μυστικό Πελάτη", + "issuerUrl": "URL εκδότη", + "authUrl": "URL εξουσιοδότησης", + "tokenUrl": "URL διακριτικού", + "userIdentifier": "Διαδρομή αναγνωριστικού χρήστη", + "displayName": "Διαδρομή ονόματος εμφάνισης", + "scopes": "Πεδία", + "userinfoUrl": "Παράκαμψη URL πληροφοριών χρήστη", + "groupClaim": "Ομαδική αξίωση", + "groupClaimDesc": "Προαιρετικό. Η διαδρομή αξίωσης που περιέχει τις ομάδες του χρήστη. Η προεπιλεγμένη τιμή είναι ομάδες, ρόλοι και στη συνέχεια ομάδα. Χρησιμοποιήστε αυτήν τη διαδρομή για παρόχους με προσαρμοσμένη αξίωση (π.χ. Zitadel).", + "allowedUsers": "Επιτρεπόμενοι χρήστες", + "allowedUsersDesc": "Ένα email ανά γραμμή. Αφήστε το κενό για να επιτρέπονται όλα.", + "adminGroup": "Ομάδα Διαχειριστών", + "adminGroupDesc": "Οι χρήστες σε αυτήν την ομάδα έχουν παραχωρηθεί ως διαχειριστές. Αφήστε το κενό για να απενεργοποιήσετε τον συγχρονισμό ομάδας.", + "caCert": "Προσαρμοσμένο Πιστοποιητικό CA", + "caCertDesc": "Προαιρετικό. Πιστοποιητικό CA με κωδικοποίηση PEM για παρόχους OIDC που χρησιμοποιούν ιδιωτική ή αυτο-υπογεγραμμένη αρχή έκδοσης πιστοποιητικών. Αφήστε το κενό για να χρησιμοποιήσετε τον χώρο αποθήκευσης αξιοπιστίας του συστήματος." + } +} diff --git a/plugins/sso/locales/translated/es_ES.json b/plugins/sso/locales/translated/es_ES.json new file mode 100644 index 000000000..201e673a7 --- /dev/null +++ b/plugins/sso/locales/translated/es_ES.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "Iniciar sesión con {{name}}", + "loading": "Cargando...", + "providers": { + "docsLink": "Ver documentación", + "none": "No hay proveedores SSO configurados.", + "add": "Añadir proveedor", + "edit": "Editar proveedor", + "delete": "Eliminar proveedor", + "deleteConfirm": "¿Eliminar este proveedor? Los usuarios asociados no podrán iniciar sesión.", + "name": "Nombre visible", + "type": "Tipo de proveedor", + "enabled": "Activado", + "save": "Guardar proveedor", + "saving": "Guardando...", + "saved": "Guardado", + "saveFailed": "Error al guardar", + "deleted": "Eliminado", + "deleteFailed": "Error al eliminar", + "cancel": "Cancelar" + }, + "fields": { + "clientId": "ID de cliente", + "clientSecret": "Secreto de cliente", + "issuerUrl": "URL del emisor", + "authUrl": "URL de autorización", + "tokenUrl": "URL del token", + "userIdentifier": "Ruta del identificador de usuario", + "displayName": "Ruta del nombre para mostrar", + "scopes": "Ámbitos", + "userinfoUrl": "Sobrescribir URL de Userinfo", + "groupClaim": "Claim de grupo", + "groupClaimDesc": "Opcional. La ruta del claim que contiene los grupos del usuario. Por defecto, se usan 'groups', 'roles' y luego 'group'. Úselo para proveedores con un claim personalizado (p. ej. Zitadel).", + "allowedUsers": "Usuarios permitidos", + "allowedUsersDesc": "Un correo electrónico por línea. Déjelo vacío para permitir todos.", + "adminGroup": "Grupo de administradores", + "adminGroupDesc": "Los usuarios de este grupo reciben permisos de administrador. Déjelo vacío para desactivar la sincronización de grupos.", + "caCert": "Certificado CA personalizado", + "caCertDesc": "Opcional. Certificado CA codificado en PEM para proveedores OIDC que utilicen una CA privada o autofirmada. Déjelo vacío para usar el almacén de confianza del sistema." + } +} diff --git a/plugins/sso/locales/translated/fi_FI.json b/plugins/sso/locales/translated/fi_FI.json new file mode 100644 index 000000000..0c40052ac --- /dev/null +++ b/plugins/sso/locales/translated/fi_FI.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "Kirjaudu sisään tunnuksella {{name}}", + "loading": "Ladataan...", + "providers": { + "docsLink": "Näytä dokumentit", + "none": "SSO-palveluntarjoajia ei ole määritetty.", + "add": "Lisää palveluntarjoaja", + "edit": "Muokkaa palveluntarjoajaa", + "delete": "Poista palveluntarjoaja", + "deleteConfirm": "Poistetaanko tämä palveluntarjoaja? Liitännäiset käyttäjät eivät voi kirjautua sisään.", + "name": "Näyttönimi", + "type": "Palveluntarjoajan tyyppi", + "enabled": "Käytössä", + "save": "Tallenna palveluntarjoaja", + "saving": "Tallennetaan...", + "saved": "Tallennettu", + "saveFailed": "Tallennus epäonnistui", + "deleted": "Poistettu", + "deleteFailed": "Poistaminen epäonnistui", + "cancel": "Peruuttaa" + }, + "fields": { + "clientId": "Asiakastunnus", + "clientSecret": "Asiakkaan salaisuus", + "issuerUrl": "Liikkeeseenlaskijan URL-osoite", + "authUrl": "Valtuutus-URL-osoite", + "tokenUrl": "Tunnuksen URL-osoite", + "userIdentifier": "Käyttäjätunnisteen polku", + "displayName": "Näyttönimen polku", + "scopes": "Laajuusalueet", + "userinfoUrl": "Käyttäjätietojen URL-osoitteen ohitus", + "groupClaim": "Ryhmävaatimus", + "groupClaimDesc": "Valinnainen. Vaatimuspolku, joka sisältää käyttäjän ryhmät. Oletusarvo on ryhmät, roolit ja lopuksi ryhmä. Käytä tätä palveluntarjoajille, joilla on mukautettu vaatimus (esim. Zitadel).", + "allowedUsers": "Sallitut käyttäjät", + "allowedUsersDesc": "Yksi sähköpostiosoite riviä kohden. Jätä tyhjäksi salliaksesi kaikki.", + "adminGroup": "Hallintaryhmä", + "adminGroupDesc": "Tämän ryhmän käyttäjille on myönnetty järjestelmänvalvojan oikeudet. Jätä tyhjäksi, jos haluat poistaa ryhmän synkronoinnin käytöstä.", + "caCert": "Mukautettu CA-varmenne", + "caCertDesc": "Valinnainen. PEM-koodattu CA-varmenne OIDC-palveluntarjoajille, jotka käyttävät yksityistä tai itse allekirjoitettua CA:ta. Jätä tyhjäksi, jos haluat käyttää järjestelmän luotettavuussäilöä." + } +} diff --git a/plugins/sso/locales/translated/fr_FR.json b/plugins/sso/locales/translated/fr_FR.json new file mode 100644 index 000000000..ac15de243 --- /dev/null +++ b/plugins/sso/locales/translated/fr_FR.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "Se connecter avec {{name}}", + "loading": "Chargement...", + "providers": { + "docsLink": "Documentation", + "none": "Aucun fournisseur SSO configuré.", + "add": "Ajouter un fournisseur", + "edit": "Modifier le fournisseur", + "delete": "Supprimer le fournisseur", + "deleteConfirm": "Supprimer ce fournisseur ? Les utilisateurs associés ne pourront plus se connecter.", + "name": "Nom d’affichage", + "type": "Type de fournisseur", + "enabled": "Activé", + "save": "Enregistrer le fournisseur", + "saving": "Enregistrement…", + "saved": "Enregistré", + "saveFailed": "Échec de l'enregistrement", + "deleted": "Supprimé", + "deleteFailed": "Échec de la suppression", + "cancel": "Annuler" + }, + "fields": { + "clientId": "ID client", + "clientSecret": "Secret client", + "issuerUrl": "URL de l'émetteur", + "authUrl": "URL d'autorisation", + "tokenUrl": "URL du jeton", + "userIdentifier": "Chemin de l'identifiant utilisateur", + "displayName": "Chemin du nom affiché", + "scopes": "Scopes", + "userinfoUrl": "URL Userinfo personnalisée", + "groupClaim": "Revendication de groupe", + "groupClaimDesc": "Facultatif. Chemin de la revendication contenant les groupes de l'utilisateur. Par défaut : groups, roles, puis group. Utilisez ce champ pour les fournisseurs avec une revendication personnalisée (ex. Zitadel).", + "allowedUsers": "Utilisateurs autorisés", + "allowedUsersDesc": "Une adresse e-mail par ligne. Laissez vide pour autoriser tout le monde.", + "adminGroup": "Groupe administrateur", + "adminGroupDesc": "Les utilisateurs de ce groupe ont les droits administrateur. Laissez vide pour désactiver la synchronisation de groupe.", + "caCert": "Certificat CA personnalisé", + "caCertDesc": "Facultatif. Certificat CA au format PEM pour les fournisseurs OIDC utilisant une CA privée ou auto-signée. Laissez vide pour utiliser le magasin de confiance du système." + } +} diff --git a/plugins/sso/locales/translated/he_IL.json b/plugins/sso/locales/translated/he_IL.json new file mode 100644 index 000000000..b9c0b5321 --- /dev/null +++ b/plugins/sso/locales/translated/he_IL.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "התחברות באמצעות {{name}}", + "loading": "טְעִינָה...", + "providers": { + "docsLink": "הצג מסמכים", + "none": "לא הוגדרו ספקי SSO.", + "add": "הוסף ספק", + "edit": "עריכת ספק", + "delete": "מחיקת ספק", + "deleteConfirm": "למחוק ספק זה? משתמשים משויכים לא יוכלו להתחבר.", + "name": "שם תצוגה", + "type": "סוג ספק", + "enabled": "מופעל", + "save": "שמור ספק", + "saving": "חִסָכוֹן...", + "saved": "נשמר", + "saveFailed": "השמירה נכשלה", + "deleted": "נמחק", + "deleteFailed": "המחיקה נכשלה", + "cancel": "לְבַטֵל" + }, + "fields": { + "clientId": "מזהה לקוח", + "clientSecret": "סוד הלקוח", + "issuerUrl": "כתובת URL של המנפיק", + "authUrl": "כתובת אתר להרשאה", + "tokenUrl": "כתובת אתר של אסימון", + "userIdentifier": "נתיב מזהה המשתמש", + "displayName": "נתיב שם התצוגה", + "scopes": "טווחים", + "userinfoUrl": "עקיפת כתובת URL של פרטי משתמש", + "groupClaim": "תביעה קבוצתית", + "groupClaimDesc": "אופציונלי. נתיב התביעה המכיל את קבוצות המשתמש. ברירת המחדל היא קבוצות, תפקידים ולאחר מכן קבוצה. השתמש באפשרות זו עבור ספקים עם תביעה מותאמת אישית (למשל Zitadel).", + "allowedUsers": "משתמשים מורשים", + "allowedUsersDesc": "אימייל אחד בכל שורה. יש להשאיר ריק כדי לאפשר את כולם.", + "adminGroup": "קבוצת ניהול", + "adminGroupDesc": "למשתמשים בקבוצה זו יש הרשאה לניהול. יש להשאיר ריק כדי להשבית את סנכרון הקבוצה.", + "caCert": "תעודת CA מותאמת אישית", + "caCertDesc": "אופציונלי. אישור CA מקודד PEM עבור ספקי OIDC המשתמשים באישור CA פרטי או חתום עצמית. השאר ריק כדי להשתמש במאגר האמון של המערכת." + } +} diff --git a/plugins/sso/locales/translated/hi_IN.json b/plugins/sso/locales/translated/hi_IN.json new file mode 100644 index 000000000..4d2777ebf --- /dev/null +++ b/plugins/sso/locales/translated/hi_IN.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "{{name}} से लॉगिन करें", + "loading": "लोड हो रहा है...", + "providers": { + "docsLink": "दस्तावेज़ देखें", + "none": "कोई SSO प्रदाता कॉन्फ़िगर नहीं है।", + "add": "प्रदाता जोड़ें", + "edit": "प्रदाता संपादित करें", + "delete": "प्रदाता हटाएँ", + "deleteConfirm": "इस प्रदाता को हटाएँ? इससे जुड़े उपयोगकर्ता लॉगिन नहीं कर पाएंगे।", + "name": "प्रदर्शन नाम", + "type": "प्रदाता प्रकार", + "enabled": "सक्षम", + "save": "प्रदाता सहेजें", + "saving": "सहेजा जा रहा है...", + "saved": "सहेजा गया", + "saveFailed": "सहेजने में विफल", + "deleted": "हटाया गया", + "deleteFailed": "हटाने में विफल", + "cancel": "रद्द करें" + }, + "fields": { + "clientId": "क्लाइंट आईडी", + "clientSecret": "क्लाइंट सीक्रेट", + "issuerUrl": "जारीकर्ता URL", + "authUrl": "प्राधिकरण URL", + "tokenUrl": "टोकन URL", + "userIdentifier": "उपयोगकर्ता पहचानकर्ता पथ", + "displayName": "प्रदर्शन नाम पथ", + "scopes": "स्कोप", + "userinfoUrl": "Userinfo URL ओवरराइड करें", + "groupClaim": "समूह दावा", + "groupClaimDesc": "वैकल्पिक। वह दावा पथ जिसमें उपयोगकर्ता के समूह शामिल हैं। डिफ़ॉल्ट रूप से groups, roles, फिर group होता है। कस्टम दावे वाले प्रदाताओं के लिए इसका उपयोग करें (जैसे Zitadel)।", + "allowedUsers": "अनुमत उपयोगकर्ता", + "allowedUsersDesc": "प्रति पंक्ति एक ईमेल। सभी को अनुमति देने के लिए खाली छोड़ें।", + "adminGroup": "व्यवस्थापक समूह", + "adminGroupDesc": "इस समूह के उपयोगकर्ताओं को व्यवस्थापक अधिकार दिए जाते हैं। समूह सिंक अक्षम करने के लिए खाली छोड़ें।", + "caCert": "कस्टम CA प्रमाणपत्र", + "caCertDesc": "वैकल्पिक। निजी या स्व-हस्ताक्षरित CA का उपयोग करने वाले OIDC प्रदाताओं के लिए PEM-एन्कोडेड CA प्रमाणपत्र। सिस्टम ट्रस्ट स्टोर का उपयोग करने के लिए खाली छोड़ें।" + } +} diff --git a/plugins/sso/locales/translated/hu_HU.json b/plugins/sso/locales/translated/hu_HU.json new file mode 100644 index 000000000..28f92d08e --- /dev/null +++ b/plugins/sso/locales/translated/hu_HU.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "Bejelentkezés {{name}} használatával", + "loading": "Terhelés...", + "providers": { + "docsLink": "Dokumentumok megtekintése", + "none": "Nincsenek konfigurálva SSO-szolgáltatók.", + "add": "Szolgáltató hozzáadása", + "edit": "Szolgáltató szerkesztése", + "delete": "Szolgáltató törlése", + "deleteConfirm": "Törli ezt a szolgáltatót? A társított felhasználók nem fognak tudni bejelentkezni.", + "name": "Megjelenített név", + "type": "Szolgáltató típusa", + "enabled": "Engedélyezve", + "save": "Szolgáltató mentése", + "saving": "Megtakarítás...", + "saved": "Mentett", + "saveFailed": "Nem sikerült menteni", + "deleted": "Törölt", + "deleteFailed": "Nem sikerült törölni", + "cancel": "Mégsem" + }, + "fields": { + "clientId": "Ügyfél-azonosító", + "clientSecret": "Ügyféltitkos kód", + "issuerUrl": "Kibocsátó URL-címe", + "authUrl": "Engedélyezési URL", + "tokenUrl": "Token URL", + "userIdentifier": "Felhasználói azonosító útvonala", + "displayName": "Megjelenített név útvonala", + "scopes": "Hatókör", + "userinfoUrl": "Felhasználói adatok URL-címének felülbírálása", + "groupClaim": "Csoportos követelés", + "groupClaimDesc": "Opcionális. A felhasználó csoportjait tartalmazó igénylési útvonal. Alapértelmezés szerint csoportok, szerepkörök, majd csoport. Ezt egyéni igényléssel rendelkező szolgáltatóknál használja (pl. Zitadel).", + "allowedUsers": "Engedélyezett felhasználók", + "allowedUsersDesc": "Egy e-mail cím soronként. Hagyja üresen, ha az összeset engedélyezi.", + "adminGroup": "Adminisztrátori csoport", + "adminGroupDesc": "A csoport felhasználói adminisztrátori jogosultsággal rendelkeznek. Hagyja üresen a csoport szinkronizálásának letiltásához.", + "caCert": "Egyéni CA tanúsítvány", + "caCertDesc": "Opcionális. PEM-kódolású CA-tanúsítvány OIDC-szolgáltatók számára, privát vagy önaláírt CA-t használva. Hagyja üresen a rendszer megbízható tárolójának használatához." + } +} diff --git a/plugins/sso/locales/translated/id_ID.json b/plugins/sso/locales/translated/id_ID.json new file mode 100644 index 000000000..73d843ae2 --- /dev/null +++ b/plugins/sso/locales/translated/id_ID.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "Masuk dengan {{name}}", + "loading": "Memuat...", + "providers": { + "docsLink": "Lihat dokumen", + "none": "Tidak ada penyedia SSO yang dikonfigurasi.", + "add": "Tambahkan Penyedia", + "edit": "Edit Penyedia", + "delete": "Hapus Penyedia", + "deleteConfirm": "Hapus penyedia ini? Pengguna terkait tidak akan dapat masuk.", + "name": "Nama Tampilan", + "type": "Jenis Penyedia", + "enabled": "Diaktifkan", + "save": "Simpan Penyedia", + "saving": "Penghematan...", + "saved": "Tersimpan", + "saveFailed": "Gagal menyimpan", + "deleted": "Dihapus", + "deleteFailed": "Gagal menghapus", + "cancel": "Membatalkan" + }, + "fields": { + "clientId": "ID Klien", + "clientSecret": "Rahasia Klien", + "issuerUrl": "URL Penerbit", + "authUrl": "URL Otorisasi", + "tokenUrl": "URL Token", + "userIdentifier": "Jalur Pengidentifikasi Pengguna", + "displayName": "Jalur Nama Tampilan", + "scopes": "Lingkup", + "userinfoUrl": "Ganti URL Informasi Pengguna", + "groupClaim": "Klaim Kelompok", + "groupClaimDesc": "Opsional. Jalur klaim yang berisi grup pengguna. Secara default berupa grup, peran, lalu grup. Gunakan ini untuk penyedia dengan klaim khusus (misalnya Zitadel).", + "allowedUsers": "Pengguna yang Diizinkan", + "allowedUsersDesc": "Satu email per baris. Biarkan kosong untuk mengizinkan semua email.", + "adminGroup": "Grup Admin", + "adminGroupDesc": "Pengguna dalam grup ini diberikan hak admin. Biarkan kosong untuk menonaktifkan sinkronisasi grup.", + "caCert": "Sertifikat CA Kustom", + "caCertDesc": "Opsional. Sertifikat CA berkode PEM untuk penyedia OIDC yang menggunakan CA pribadi atau yang ditandatangani sendiri. Biarkan kosong untuk menggunakan penyimpanan kepercayaan sistem." + } +} diff --git a/plugins/sso/locales/translated/it_IT.json b/plugins/sso/locales/translated/it_IT.json new file mode 100644 index 000000000..90637724d --- /dev/null +++ b/plugins/sso/locales/translated/it_IT.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "Accedi con {{name}}", + "loading": "Caricamento in corso...", + "providers": { + "docsLink": "Visualizza documentazione", + "none": "Nessun provider SSO configurato.", + "add": "Aggiungi provider", + "edit": "Modifica provider", + "delete": "Elimina provider", + "deleteConfirm": "Eliminare questo provider? Gli utenti associati non potranno più accedere.", + "name": "Nome visualizzato", + "type": "Tipo provider", + "enabled": "Abilitato", + "save": "Salva provider", + "saving": "Salvataggio in corso...", + "saved": "Salvato", + "saveFailed": "Salvataggio non riuscito", + "deleted": "Eliminato", + "deleteFailed": "Eliminazione non riuscita", + "cancel": "Annulla" + }, + "fields": { + "clientId": "ID client", + "clientSecret": "Segreto client", + "issuerUrl": "URL dell'emittente", + "authUrl": "URL di autorizzazione", + "tokenUrl": "URL token", + "userIdentifier": "Percorso identificatore utente", + "displayName": "Percorso nome visualizzato", + "scopes": "Ambiti", + "userinfoUrl": "Sovrascrivi URL Userinfo", + "groupClaim": "Attributo gruppo", + "groupClaimDesc": "Opzionale. Il percorso dell'attributo contenente i gruppi dell'utente. Il valore predefinito è groups, roles, quindi group. Da utilizzare per provider con un attributo personalizzato (es. Zitadel).", + "allowedUsers": "Utenti consentiti", + "allowedUsersDesc": "Un'email per riga. Lascia vuoto per consentire a tutti.", + "adminGroup": "Gruppo amministratori", + "adminGroupDesc": "Gli utenti di questo gruppo ottengono i diritti di amministratore. Lascia vuoto per disabilitare la sincronizzazione dei gruppi.", + "caCert": "Certificato CA personalizzato", + "caCertDesc": "Opzionale. Certificato CA in formato PEM per provider OIDC che utilizzano una CA privata o autofirmata. Lascia vuoto per usare l'archivio dei certificati attendibili di sistema." + } +} diff --git a/plugins/sso/locales/translated/ja_JP.json b/plugins/sso/locales/translated/ja_JP.json new file mode 100644 index 000000000..fd218332b --- /dev/null +++ b/plugins/sso/locales/translated/ja_JP.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "{{name}}でログイン", + "loading": "読み込み中...", + "providers": { + "docsLink": "ドキュメントを表示", + "none": "SSOプロバイダーが設定されていません。", + "add": "追加", + "edit": "編集", + "delete": "削除", + "deleteConfirm": "このプロバイダーを削除しますか?関連付けられているユーザーはログインできなくなります。", + "name": "表示名", + "type": "プロバイダータイプ", + "enabled": "有効", + "save": "保存", + "saving": "保存中...", + "saved": "保存しました", + "saveFailed": "保存に失敗しました", + "deleted": "削除しました", + "deleteFailed": "削除に失敗しました", + "cancel": "キャンセル" + }, + "fields": { + "clientId": "クライアントID", + "clientSecret": "クライアントシークレット", + "issuerUrl": "発行者URL", + "authUrl": "認証URL", + "tokenUrl": "トークンURL", + "userIdentifier": "ユーザー識別子パス", + "displayName": "表示名パス", + "scopes": "スコープ", + "userinfoUrl": "Userinfo URLの上書き", + "groupClaim": "グループクレーム", + "groupClaimDesc": "オプション。ユーザーのグループを含むクレームパス。デフォルトは groups, roles, group の順。カスタムクレームを使うプロバイダー用 (例: Zitadel)。", + "allowedUsers": "許可ユーザー", + "allowedUsersDesc": "1行に1つのメールアドレス。空の場合は全員許可。", + "adminGroup": "管理者グループ", + "adminGroupDesc": "このグループのユーザーは管理者権限が付与されます。空にするとグループ同期は無効です。", + "caCert": "カスタムCA証明書", + "caCertDesc": "オプション。プライベートまたは自己署名CAを使用するOIDCプロバイダー用のPEMエンコードCA証明書。空の場合はシステムの信頼ストアを使用。" + } +} diff --git a/plugins/sso/locales/translated/ko_KR.json b/plugins/sso/locales/translated/ko_KR.json new file mode 100644 index 000000000..0234c8821 --- /dev/null +++ b/plugins/sso/locales/translated/ko_KR.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "{{name}}으로 로그인", + "loading": "로딩 중...", + "providers": { + "docsLink": "문서 보기", + "none": "설정된 SSO 제공자가 없습니다.", + "add": "제공자 추가", + "edit": "제공자 편집", + "delete": "제공자 삭제", + "deleteConfirm": "이 제공자를 삭제하시겠습니까? 연결된 사용자는 로그인할 수 없습니다.", + "name": "표시 이름", + "type": "제공자 유형", + "enabled": "사용", + "save": "제공자 저장", + "saving": "저장 중...", + "saved": "저장됨", + "saveFailed": "저장 실패", + "deleted": "삭제됨", + "deleteFailed": "삭제 실패", + "cancel": "취소" + }, + "fields": { + "clientId": "클라이언트 ID", + "clientSecret": "클라이언트 시크릿", + "issuerUrl": "Issuer URL", + "authUrl": "인증 URL", + "tokenUrl": "토큰 URL", + "userIdentifier": "사용자 식별자 경로", + "displayName": "표시 이름 경로", + "scopes": "스코프", + "userinfoUrl": "Userinfo URL 재정의", + "groupClaim": "그룹 클레임", + "groupClaimDesc": "선택 사항입니다. 사용자 그룹이 포함된 클레임 경로입니다. 기본값은 groups, roles, group 순서로 검색됩니다. 사용자 정의 클레임이 있는 제공자(예: Zitadel)의 경우 이 값을 사용하세요.", + "allowedUsers": "허용된 사용자", + "allowedUsersDesc": "한 줄에 이메일 하나씩 입력하세요. 비워두면 모두 허용됩니다.", + "adminGroup": "관리자 그룹", + "adminGroupDesc": "이 그룹의 사용자에게 관리자 권한이 부여됩니다. 비워두면 그룹 동기화가 비활성화됩니다.", + "caCert": "사용자 정의 CA 인증서", + "caCertDesc": "선택 사항입니다. 개인 또는 자체 서명 CA를 사용하는 OIDC 제공자를 위한 PEM 인코딩 CA 인증서입니다. 비워두면 시스템 신뢰 저장소가 사용됩니다." + } +} diff --git a/plugins/sso/locales/translated/nl_NL.json b/plugins/sso/locales/translated/nl_NL.json new file mode 100644 index 000000000..01cba1a33 --- /dev/null +++ b/plugins/sso/locales/translated/nl_NL.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "Inloggen met {{name}}", + "loading": "Laden...", + "providers": { + "docsLink": "Documenten bekijken", + "none": "Er zijn geen SSO-providers geconfigureerd.", + "add": "Aanbieder toevoegen", + "edit": "Bewerk provider", + "delete": "Provider verwijderen", + "deleteConfirm": "Deze provider verwijderen? Gekoppelde gebruikers kunnen dan niet meer inloggen.", + "name": "Weergavenaam", + "type": "Aanbiedertype", + "enabled": "Ingeschakeld", + "save": "Aanbieder opslaan", + "saving": "Besparing...", + "saved": "Opgeslagen", + "saveFailed": "Opslaan mislukt", + "deleted": "Verwijderd", + "deleteFailed": "Verwijderen mislukt", + "cancel": "Annuleren" + }, + "fields": { + "clientId": "Klant-ID", + "clientSecret": "Klantgeheim", + "issuerUrl": "URL van de uitgever", + "authUrl": "Autorisatie-URL", + "tokenUrl": "Token-URL", + "userIdentifier": "Gebruikersidentificatiepad", + "displayName": "Weergavenaam Pad", + "scopes": "Scopes", + "userinfoUrl": "Gebruikersinfo-URL overschrijven", + "groupClaim": "Groepsclaim", + "groupClaimDesc": "Optioneel. Het pad naar de claim dat de groepen van de gebruiker bevat. Standaard is dit: groepen, rollen, vervolgens groep. Gebruik dit voor providers met een aangepaste claim (bijv. Zitadel).", + "allowedUsers": "Toegestane gebruikers", + "allowedUsersDesc": "Eén e-mailadres per regel. Laat dit veld leeg om alle e-mailadressen toe te staan.", + "adminGroup": "Beheerdersgroep", + "adminGroupDesc": "Gebruikers in deze groep hebben beheerdersrechten. Laat dit veld leeg om groepssynchronisatie uit te schakelen.", + "caCert": "Aangepast CA-certificaat", + "caCertDesc": "Optioneel. PEM-gecodeerd CA-certificaat voor OIDC-providers die gebruikmaken van een privé- of zelfondertekende CA. Laat dit veld leeg om het systeemvertrouwensarchief te gebruiken." + } +} diff --git a/plugins/sso/locales/translated/no_NO.json b/plugins/sso/locales/translated/no_NO.json new file mode 100644 index 000000000..c050fa974 --- /dev/null +++ b/plugins/sso/locales/translated/no_NO.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "Logg inn med {{name}}", + "loading": "Laster inn...", + "providers": { + "docsLink": "Vis dokumenter", + "none": "Ingen SSO-leverandører er konfigurert.", + "add": "Legg til leverandør", + "edit": "Rediger leverandør", + "delete": "Slett leverandør", + "deleteConfirm": "Vil du slette denne leverandøren? Tilknyttede brukere vil ikke kunne logge inn.", + "name": "Visningsnavn", + "type": "Leverandørtype", + "enabled": "Aktivert", + "save": "Lagre leverandør", + "saving": "Lagrer...", + "saved": "Lagret", + "saveFailed": "Kunne ikke lagre", + "deleted": "Slettet", + "deleteFailed": "Kunne ikke slette", + "cancel": "Kansellere" + }, + "fields": { + "clientId": "Klient-ID", + "clientSecret": "Klienthemmelighet", + "issuerUrl": "Utsteder-URL", + "authUrl": "Autorisasjons-URL", + "tokenUrl": "Token-URL", + "userIdentifier": "Brukeridentifikatorsti", + "displayName": "Sti til visningsnavn", + "scopes": "Omfang", + "userinfoUrl": "Overstyr brukerinfo-URL", + "groupClaim": "Gruppekrav", + "groupClaimDesc": "Valgfritt. Kravbanen som inneholder brukerens grupper. Standardinnstillingen er grupper, roller og deretter gruppe. Bruk dette for leverandører med et tilpasset krav (f.eks. Zitadel).", + "allowedUsers": "Tillatte brukere", + "allowedUsersDesc": "Én e-post per linje. La stå tomt for å tillate alle.", + "adminGroup": "Admingruppe", + "adminGroupDesc": "Brukere i denne gruppen har administratorrettigheter. La feltet stå tomt for å deaktivere gruppesynkronisering.", + "caCert": "Tilpasset CA-sertifikat", + "caCertDesc": "Valgfritt. PEM-kodet CA-sertifikat for OIDC-leverandører som bruker en privat eller selvsignert CA. La stå tomt for å bruke systemets klareringslager." + } +} diff --git a/plugins/sso/locales/translated/pl_PL.json b/plugins/sso/locales/translated/pl_PL.json new file mode 100644 index 000000000..23a7bc5cc --- /dev/null +++ b/plugins/sso/locales/translated/pl_PL.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "Zaloguj się za pomocą {{name}}", + "loading": "Ładowanie...", + "providers": { + "docsLink": "Zobacz dokumentację", + "none": "Nie skonfigurowano dostawców SSO.", + "add": "Dodaj dostawcę", + "edit": "Edytuj dostawcę", + "delete": "Usuń dostawcę", + "deleteConfirm": "Usunąć tego dostawcę? Powiązani użytkownicy nie będą mogli się zalogować.", + "name": "Nazwa wyświetlana", + "type": "Typ dostawcy", + "enabled": "Włączone", + "save": "Zapisz dostawcę", + "saving": "Zapisywanie...", + "saved": "Zapisano", + "saveFailed": "Nie udało się zapisać", + "deleted": "Usunięto", + "deleteFailed": "Nie udało się usunąć", + "cancel": "Anuluj" + }, + "fields": { + "clientId": "Identyfikator klienta", + "clientSecret": "Sekret klienta", + "issuerUrl": "Adres URL wystawcy", + "authUrl": "Adres URL autoryzacji", + "tokenUrl": "Adres URL tokena", + "userIdentifier": "Ścieżka identyfikatora użytkownika", + "displayName": "Ścieżka nazwy wyświetlanej", + "scopes": "Zakresy", + "userinfoUrl": "Zastąp adres URL informacji o użytkowniku", + "groupClaim": "Claim grupy", + "groupClaimDesc": "Opcjonalnie. Ścieżka claimu zawierającego grupy użytkownika. Domyślnie: groups, roles, potem group. Użyj tego dla dostawców z niestandardowym claimem (np. Zitadel).", + "allowedUsers": "Dozwoleni użytkownicy", + "allowedUsersDesc": "Jeden adres e-mail w każdym wierszu. Pozostaw puste, aby zezwolić na wszystkie.", + "adminGroup": "Grupa administratorów", + "adminGroupDesc": "Użytkownicy w tej grupie otrzymują uprawnienia administratora. Pozostaw puste, aby wyłączyć synchronizację grupy.", + "caCert": "Niestandardowy certyfikat CA", + "caCertDesc": "Opcjonalnie. Certyfikat CA w formacie PEM dla dostawców OIDC korzystających z prywatnego lub samopodpisanego CA. Pozostaw puste, aby użyć systemowego magazynu zaufanych certyfikatów." + } +} diff --git a/plugins/sso/locales/translated/pt_BR.json b/plugins/sso/locales/translated/pt_BR.json new file mode 100644 index 000000000..60faf5dbb --- /dev/null +++ b/plugins/sso/locales/translated/pt_BR.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "Faça login com {{name}}", + "loading": "Carregando...", + "providers": { + "docsLink": "Ver documentos", + "none": "Nenhum provedor de SSO configurado.", + "add": "Adicionar provedor", + "edit": "Editar provedor", + "delete": "Excluir provedor", + "deleteConfirm": "Excluir este provedor? Os usuários associados não poderão fazer login.", + "name": "Nome de exibição", + "type": "Tipo de provedor", + "enabled": "Habilitado", + "save": "Salvar provedor", + "saving": "Salvando...", + "saved": "Salvo", + "saveFailed": "Falha ao salvar", + "deleted": "Excluído", + "deleteFailed": "Falha ao excluir", + "cancel": "Cancelar" + }, + "fields": { + "clientId": "ID do cliente", + "clientSecret": "Segredo do cliente", + "issuerUrl": "URL do emissor", + "authUrl": "URL de autorização", + "tokenUrl": "URL do token", + "userIdentifier": "Caminho do identificador do usuário", + "displayName": "Nome de exibição Caminho", + "scopes": "Escopos", + "userinfoUrl": "Substituir URL de informações do usuário", + "groupClaim": "Reivindicação de grupo", + "groupClaimDesc": "Opcional. O caminho da declaração que contém os grupos do usuário. O padrão é grupos, funções e, em seguida, grupo. Use isso para provedores com uma declaração personalizada (por exemplo, Zitadel).", + "allowedUsers": "Usuários autorizados", + "allowedUsersDesc": "Um e-mail por linha. Deixe em branco para permitir todos.", + "adminGroup": "Grupo de Administração", + "adminGroupDesc": "Os usuários deste grupo têm privilégios de administrador. Deixe em branco para desativar a sincronização do grupo.", + "caCert": "Certificado CA personalizado", + "caCertDesc": "Opcional. Certificado CA codificado em PEM para provedores OIDC que utilizam uma CA privada ou autoassinada. Deixe em branco para usar o repositório de certificados confiáveis do sistema." + } +} diff --git a/plugins/sso/locales/translated/pt_PT.json b/plugins/sso/locales/translated/pt_PT.json new file mode 100644 index 000000000..7d0a82157 --- /dev/null +++ b/plugins/sso/locales/translated/pt_PT.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "Iniciar sessão com {{name}}", + "loading": "A carregar...", + "providers": { + "docsLink": "Ver docs", + "none": "Nenhum provedor de SSO configurado.", + "add": "Adicionar Provedor", + "edit": "Editar Provedor", + "delete": "Eliminar Provedor", + "deleteConfirm": "Eliminar este provedor? Os utilizadores associados não poderão iniciar sessão.", + "name": "Nome de apresentação", + "type": "Tipo de Provedor", + "enabled": "Ativado", + "save": "Guardar Provedor", + "saving": "A guardar...", + "saved": "Guardado", + "saveFailed": "Falha ao guardar", + "deleted": "Eliminado", + "deleteFailed": "Falha ao eliminar", + "cancel": "Cancelar" + }, + "fields": { + "clientId": "ID do cliente", + "clientSecret": "Segredo do cliente", + "issuerUrl": "URL do emissor", + "authUrl": "URL de autorização", + "tokenUrl": "URL do token", + "userIdentifier": "Caminho do identificador do utilizador", + "displayName": "Caminho do nome de exibição", + "scopes": "Âmbitos", + "userinfoUrl": "Substituir URL do Userinfo", + "groupClaim": "Atributo de grupo", + "groupClaimDesc": "Opcional. O caminho do atributo que contém os grupos do utilizador. Predefinido para groups, roles e depois group. Utilize para fornecedores com um atributo personalizado (ex.: Zitadel).", + "allowedUsers": "Utilizadores permitidos", + "allowedUsersDesc": "Um email por linha. Deixe em branco para permitir todos.", + "adminGroup": "Grupo de administração", + "adminGroupDesc": "Os utilizadores neste grupo recebem privilégios de administrador. Deixe em branco para desativar a sincronização de grupos.", + "caCert": "Certificado CA personalizado", + "caCertDesc": "Opcional. Certificado CA codificado em PEM para fornecedores OIDC que utilizam uma CA privada ou autoassinada. Deixe em branco para utilizar o repositório de confiança do sistema." + } +} diff --git a/plugins/sso/locales/translated/ro_RO.json b/plugins/sso/locales/translated/ro_RO.json new file mode 100644 index 000000000..2434bb1e6 --- /dev/null +++ b/plugins/sso/locales/translated/ro_RO.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "Autentificare cu {{name}}", + "loading": "Încărcare...", + "providers": { + "docsLink": "Vizualizați documentele", + "none": "Nu sunt configurați furnizori SSO.", + "add": "Adăugați furnizor", + "edit": "Editați furnizorul", + "delete": "Ștergeți furnizorul", + "deleteConfirm": "Ștergeți acest furnizor? Utilizatorii asociați nu se vor putea conecta.", + "name": "Numele de afișare", + "type": "Tipul furnizorului", + "enabled": "Activat", + "save": "Salvați furnizorul", + "saving": "Economisire...", + "saved": "Salvat", + "saveFailed": "Salvarea nu a reușit", + "deleted": "Șters", + "deleteFailed": "Ștergerea nu a reușit", + "cancel": "Anula" + }, + "fields": { + "clientId": "ID client", + "clientSecret": "Secretul clientului", + "issuerUrl": "URL-ul emitentului", + "authUrl": "URL de autorizare", + "tokenUrl": "Adresa URL a jetonului", + "userIdentifier": "Calea de identificare a utilizatorului", + "displayName": "Calea numelui afișat", + "scopes": "Scopuri", + "userinfoUrl": "Suprascrieți adresa URL a informațiilor utilizatorului", + "groupClaim": "Revendicare de grup", + "groupClaimDesc": "Opțional. Calea de revendicare care conține grupurile utilizatorului. Implicit, se utilizează grupuri, roluri, apoi grup. Folosiți această opțiune pentru furnizorii cu o revendicare personalizată (de exemplu, Zitadel).", + "allowedUsers": "Utilizatori permiși", + "allowedUsersDesc": "Un e-mail pe rând. Lăsați necompletat pentru a permite accesul tuturor.", + "adminGroup": "Grup de administratori", + "adminGroupDesc": "Utilizatorii din acest grup au drepturi de administrator. Lăsați câmpul gol pentru a dezactiva sincronizarea grupului.", + "caCert": "Certificat CA personalizat", + "caCertDesc": "Opțional. Certificat CA codificat PEM pentru furnizorii OIDC care utilizează un CA privat sau autosemnat. Lăsați câmpul gol pentru a utiliza depozitul de încredere al sistemului." + } +} diff --git a/plugins/sso/locales/translated/ru_RU.json b/plugins/sso/locales/translated/ru_RU.json new file mode 100644 index 000000000..fe4764951 --- /dev/null +++ b/plugins/sso/locales/translated/ru_RU.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "Войти через {{name}}", + "loading": "Загрузка...", + "providers": { + "docsLink": "Документация", + "none": "Нет настроенных провайдеров SSO.", + "add": "Добавить провайдера", + "edit": "Редактировать провайдера", + "delete": "Удалить провайдера", + "deleteConfirm": "Удалить провайдера? Связанные пользователи не смогут войти.", + "name": "Отображаемое имя", + "type": "Тип провайдера", + "enabled": "Включено", + "save": "Сохранить провайдера", + "saving": "Сохранение...", + "saved": "Сохранено", + "saveFailed": "Не удалось сохранить", + "deleted": "Удалено", + "deleteFailed": "Не удалось удалить", + "cancel": "Отмена" + }, + "fields": { + "clientId": "Client ID", + "clientSecret": "Client Secret", + "issuerUrl": "URL издателя", + "authUrl": "URL авторизации", + "tokenUrl": "URL токена", + "userIdentifier": "Путь к идентификатору пользователя", + "displayName": "Путь к отображаемому имени", + "scopes": "Области", + "userinfoUrl": "Переопределить URL Userinfo", + "groupClaim": "Утверждение группы", + "groupClaimDesc": "Необязательно. Путь к утверждению, содержащему группы пользователя. По умолчанию проверяются groups, roles, затем group. Используйте для провайдеров с настраиваемым утверждением (например, Zitadel).", + "allowedUsers": "Разрешенные пользователи", + "allowedUsersDesc": "По одному email на строку. Оставьте пустым, чтобы разрешить всем.", + "adminGroup": "Группа администраторов", + "adminGroupDesc": "Пользователи в этой группе получают права администратора. Оставьте пустым, чтобы отключить синхронизацию групп.", + "caCert": "Пользовательский сертификат ЦС", + "caCertDesc": "Необязательно. Сертификат ЦС в формате PEM для провайдеров OIDC, использующих частный или самоподписанный ЦС. Оставьте пустым, чтобы использовать системное хранилище доверенных сертификатов." + } +} diff --git a/plugins/sso/locales/translated/sr_SP.json b/plugins/sso/locales/translated/sr_SP.json new file mode 100644 index 000000000..76a91b72b --- /dev/null +++ b/plugins/sso/locales/translated/sr_SP.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "Пријавите се са {{name}}", + "loading": "Учитавање...", + "providers": { + "docsLink": "Прикажи документе", + "none": "Није конфигурисан ниједан SSO добављач.", + "add": "Додај добављача", + "edit": "Измени добављача", + "delete": "Обриши добављача", + "deleteConfirm": "Да ли желите да обришете овог добављача? Повезани корисници неће моћи да се пријаве.", + "name": "Приказано име", + "type": "Тип добављача", + "enabled": "Омогућено", + "save": "Сачувај добављача", + "saving": "Чување...", + "saved": "Сачувано", + "saveFailed": "Чување није успело", + "deleted": "Избрисано", + "deleteFailed": "Брисање није успело", + "cancel": "Откажи" + }, + "fields": { + "clientId": "ИД клијента", + "clientSecret": "Тајна клијентска порука", + "issuerUrl": "URL издаваоца", + "authUrl": "URL за ауторизацију", + "tokenUrl": "URL токена", + "userIdentifier": "Путања идентификатора корисника", + "displayName": "Путања приказаног имена", + "scopes": "Опсези", + "userinfoUrl": "Замени URL са корисничким информацијама", + "groupClaim": "Групни захтев", + "groupClaimDesc": "Опционо. Путања захтева која садржи групе корисника. Подразумевано је да су групе, улоге, па група. Користите ово за добављаче са прилагођеним захтевом (нпр. Zitadel).", + "allowedUsers": "Дозвољени корисници", + "allowedUsersDesc": "Једна имејл адреса по реду. Оставите празно да бисте дозволили све.", + "adminGroup": "Администраторска група", + "adminGroupDesc": "Корисницима у овој групи је додељена администраторска позиција. Оставите празно да бисте онемогућили синхронизацију групе.", + "caCert": "Прилагођени CA сертификат", + "caCertDesc": "Опционо. PEM-кодирани CA сертификат за OIDC добављаче који користе приватни или самопотписани CA. Оставите празно да бисте користили складиште поверења система." + } +} diff --git a/plugins/sso/locales/translated/sv_SE.json b/plugins/sso/locales/translated/sv_SE.json new file mode 100644 index 000000000..6ee982710 --- /dev/null +++ b/plugins/sso/locales/translated/sv_SE.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "Logga in med {{name}}", + "loading": "Belastning...", + "providers": { + "docsLink": "Visa dokument", + "none": "Inga SSO-leverantörer har konfigurerats.", + "add": "Lägg till leverantör", + "edit": "Redigera leverantör", + "delete": "Ta bort leverantör", + "deleteConfirm": "Vill du ta bort den här leverantören? Associerade användare kommer inte att kunna logga in.", + "name": "Visningsnamn", + "type": "Leverantörstyp", + "enabled": "Aktiverad", + "save": "Spara leverantör", + "saving": "Sparande...", + "saved": "Sparad", + "saveFailed": "Misslyckades med att spara", + "deleted": "Borttagen", + "deleteFailed": "Misslyckades med att radera", + "cancel": "Avboka" + }, + "fields": { + "clientId": "Klient-ID", + "clientSecret": "Klienthemlighet", + "issuerUrl": "Utgivarens URL", + "authUrl": "Auktoriserings-URL", + "tokenUrl": "Token-URL", + "userIdentifier": "Användaridentifierarens sökväg", + "displayName": "Sökväg för visningsnamn", + "scopes": "Omfattningar", + "userinfoUrl": "Åsidosätt användarinformations-URL", + "groupClaim": "Gruppanspråk", + "groupClaimDesc": "Valfritt. Anspråkssökvägen som innehåller användarens grupper. Standardinställningen är grupper, roller och sedan grupp. Använd detta för leverantörer med ett anpassat anspråk (t.ex. Zitadel).", + "allowedUsers": "Tillåtna användare", + "allowedUsersDesc": "Ett e-postmeddelande per rad. Lämna tomt för att tillåta alla.", + "adminGroup": "Administratörsgrupp", + "adminGroupDesc": "Användare i den här gruppen har administratörsrättigheter. Lämna tomt för att inaktivera gruppsynkronisering.", + "caCert": "Anpassat CA-certifikat", + "caCertDesc": "Valfritt. PEM-kodat CA-certifikat för OIDC-leverantörer som använder en privat eller självsignerad CA. Lämna tomt för att använda systemets förtroendearkiv." + } +} diff --git a/plugins/sso/locales/translated/th_TH.json b/plugins/sso/locales/translated/th_TH.json new file mode 100644 index 000000000..5907905dd --- /dev/null +++ b/plugins/sso/locales/translated/th_TH.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "เข้าสู่ระบบด้วย {{name}}", + "loading": "กำลังโหลด...", + "providers": { + "docsLink": "ดูเอกสาร", + "none": "ไม่ได้กำหนดค่าผู้ให้บริการ SSO ใดๆ ไว้", + "add": "เพิ่มผู้ให้บริการ", + "edit": "แก้ไขผู้ให้บริการ", + "delete": "ลบผู้ให้บริการ", + "deleteConfirm": "ลบผู้ให้บริการนี้หรือไม่? ผู้ใช้ที่เกี่ยวข้องจะไม่สามารถเข้าสู่ระบบได้", + "name": "ชื่อที่แสดง", + "type": "ประเภทผู้ให้บริการ", + "enabled": "เปิดใช้งาน", + "save": "บันทึกผู้ให้บริการ", + "saving": "ประหยัด...", + "saved": "บันทึกแล้ว", + "saveFailed": "บันทึกไม่สำเร็จ", + "deleted": "ลบแล้ว", + "deleteFailed": "ลบไม่สำเร็จ", + "cancel": "ยกเลิก" + }, + "fields": { + "clientId": "รหัสลูกค้า", + "clientSecret": "ความลับของลูกค้า", + "issuerUrl": "URL ของผู้ออก", + "authUrl": "URL การอนุญาต", + "tokenUrl": "URL โทเค็น", + "userIdentifier": "เส้นทางตัวระบุผู้ใช้", + "displayName": "ชื่อที่แสดง เส้นทาง", + "scopes": "กล้องส่องทางไกล", + "userinfoUrl": "แทนที่ URL ข้อมูลผู้ใช้", + "groupClaim": "การเรียกร้องค่าสินไหมทดแทนแบบกลุ่ม", + "groupClaimDesc": "ตัวเลือกเสริม เส้นทางการอ้างสิทธิ์ที่ประกอบด้วยกลุ่มของผู้ใช้ ค่าเริ่มต้นคือ กลุ่ม บทบาท แล้วจึงเป็นกลุ่ม ใช้สำหรับผู้ให้บริการที่มีการอ้างสิทธิ์แบบกำหนดเอง (เช่น Zitadel)", + "allowedUsers": "ผู้ใช้ที่ได้รับอนุญาต", + "allowedUsersDesc": "อีเมลหนึ่งฉบับต่อหนึ่งบรรทัด หากเว้นว่างไว้จะอนุญาตให้ใช้ทั้งหมด", + "adminGroup": "กลุ่มผู้ดูแลระบบ", + "adminGroupDesc": "ผู้ใช้ในกลุ่มนี้จะได้รับสิทธิ์ผู้ดูแลระบบ เว้นว่างไว้เพื่อปิดใช้งานการซิงค์กลุ่ม", + "caCert": "ใบรับรอง CA แบบกำหนดเอง", + "caCertDesc": "ตัวเลือกเสริม ใบรับรอง CA ที่เข้ารหัสแบบ PEM สำหรับผู้ให้บริการ OIDC ที่ใช้ CA ส่วนตัวหรือ CA ที่ลงนามด้วยตนเอง เว้นว่างไว้เพื่อใช้ที่เก็บใบรับรองความเชื่อถือของระบบ" + } +} diff --git a/plugins/sso/locales/translated/tr_TR.json b/plugins/sso/locales/translated/tr_TR.json new file mode 100644 index 000000000..e9163bb84 --- /dev/null +++ b/plugins/sso/locales/translated/tr_TR.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "{{name}} ile giriş yap", + "loading": "Yükleniyor...", + "providers": { + "docsLink": "Belgeleri görüntüle", + "none": "Hiçbir SSO sağlayıcısı yapılandırılmamış.", + "add": "Sağlayıcı Ekle", + "edit": "Sağlayıcıyı Düzenle", + "delete": "Sağlayıcıyı Sil", + "deleteConfirm": "Bu sağlayıcı silinsin mi? İlişkili kullanıcılar oturum açamaz.", + "name": "Görünen Ad", + "type": "Sağlayıcı Türü", + "enabled": "Etkin", + "save": "Sağlayıcıyı Kaydet", + "saving": "Kaydediliyor...", + "saved": "Kaydedildi", + "saveFailed": "Kaydedilemedi", + "deleted": "Silindi", + "deleteFailed": "Silinemedi", + "cancel": "İptal" + }, + "fields": { + "clientId": "İstemci Kimliği", + "clientSecret": "İstemci Sırrı", + "issuerUrl": "Sağlayıcı URL'si", + "authUrl": "Yetkilendirme URL'si", + "tokenUrl": "Belirteç URL'si", + "userIdentifier": "Kullanıcı Tanımlayıcı Yolu", + "displayName": "Görünen Ad Yolu", + "scopes": "Kapsamlar", + "userinfoUrl": "Userinfo URL'sini Geçersiz Kıl", + "groupClaim": "Grup Talebi", + "groupClaimDesc": "İsteğe bağlı. Kullanıcının gruplarını içeren talep yolu. Varsayılan olarak groups, roles, ardından group kullanılır. Özel talep kullanan sağlayıcılar için bunu kullanın (örn. Zitadel).", + "allowedUsers": "İzin Verilen Kullanıcılar", + "allowedUsersDesc": "Her satıra bir e-posta. Tümüne izin vermek için boş bırakın.", + "adminGroup": "Yönetici Grubu", + "adminGroupDesc": "Bu gruptaki kullanıcılara yönetici yetkisi verilir. Grup eşitlemeyi devre dışı bırakmak için boş bırakın.", + "caCert": "Özel CA Sertifikası", + "caCertDesc": "İsteğe bağlı. Özel veya kendinden imzalı CA kullanan OIDC sağlayıcıları için PEM kodlu CA sertifikası. Sistem güven deposunu kullanmak için boş bırakın." + } +} diff --git a/plugins/sso/locales/translated/uk_UA.json b/plugins/sso/locales/translated/uk_UA.json new file mode 100644 index 000000000..98b23bd5d --- /dev/null +++ b/plugins/sso/locales/translated/uk_UA.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "Увійти за допомогою {{name}}", + "loading": "Завантаження...", + "providers": { + "docsLink": "Переглянути документи", + "none": "Постачальників SSO не налаштовано.", + "add": "Додати постачальника", + "edit": "Редагувати постачальника", + "delete": "Видалити постачальника", + "deleteConfirm": "Видалити цього постачальника? Пов’язані користувачі не зможуть увійти.", + "name": "Відображуване ім'я", + "type": "Тип постачальника", + "enabled": "Увімкнено", + "save": "Зберегти постачальника", + "saving": "Збереження...", + "saved": "Збережено", + "saveFailed": "Не вдалося зберегти", + "deleted": "Видалено", + "deleteFailed": "Не вдалося видалити", + "cancel": "Скасувати" + }, + "fields": { + "clientId": "Ідентифікатор клієнта", + "clientSecret": "Секрет клієнта", + "issuerUrl": "URL-адреса емітента", + "authUrl": "URL-адреса авторизації", + "tokenUrl": "URL-адреса токена", + "userIdentifier": "Шлях ідентифікатора користувача", + "displayName": "Шлях до відображуваного імені", + "scopes": "Області застосування", + "userinfoUrl": "Перевизначити URL-адресу інформації користувача", + "groupClaim": "Групова претензія", + "groupClaimDesc": "Необов'язково. Шлях до заяви, що містить групи користувача. За замовчуванням використовуються групи, ролі, а потім група. Використовуйте це для постачальників із власною заявою (наприклад, Zitadel).", + "allowedUsers": "Дозволені користувачі", + "allowedUsersDesc": "Одна електронна адреса на рядок. Залиште поле порожнім, щоб дозволити всім.", + "adminGroup": "Група адміністраторів", + "adminGroupDesc": "Користувачам у цій групі надано права адміністратора. Залиште поле порожнім, щоб вимкнути синхронізацію групи.", + "caCert": "Спеціальний сертифікат CA", + "caCertDesc": "Необов’язково. Сертифікат CA з PEM-кодуванням для постачальників OIDC, які використовують приватний або самопідписаний CA. Залиште поле порожнім, щоб використовувати системне сховище довірених сертифікатів." + } +} diff --git a/plugins/sso/locales/translated/vi_VN.json b/plugins/sso/locales/translated/vi_VN.json new file mode 100644 index 000000000..6bfabf68e --- /dev/null +++ b/plugins/sso/locales/translated/vi_VN.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "Đăng nhập bằng {{name}}", + "loading": "Đang tải...", + "providers": { + "docsLink": "Xem tài liệu", + "none": "Chưa có nhà cung cấp SSO.", + "add": "Thêm nhà cung cấp", + "edit": "Chỉnh sửa nhà cung cấp", + "delete": "Xóa nhà cung cấp", + "deleteConfirm": "Xóa nhà cung cấp này? Người dùng liên kết sẽ không thể đăng nhập.", + "name": "Tên hiển thị", + "type": "Loại nhà cung cấp", + "enabled": "Đã bật", + "save": "Lưu nhà cung cấp", + "saving": "Đang lưu...", + "saved": "Đã lưu", + "saveFailed": "Không thể lưu", + "deleted": "Đã xóa", + "deleteFailed": "Không thể xóa", + "cancel": "Hủy" + }, + "fields": { + "clientId": "Client ID", + "clientSecret": "Client Secret", + "issuerUrl": "Issuer URL", + "authUrl": "URL ủy quyền", + "tokenUrl": "Token URL", + "userIdentifier": "Đường dẫn định danh người dùng", + "displayName": "Đường dẫn tên hiển thị", + "scopes": "Phạm vi", + "userinfoUrl": "Ghi đè URL Userinfo", + "groupClaim": "Group Claim", + "groupClaimDesc": "Tùy chọn. Đường dẫn claim chứa các nhóm của người dùng. Mặc định là groups, roles, rồi group. Sử dụng cho các nhà cung cấp có claim tùy chỉnh (ví dụ: Zitadel).", + "allowedUsers": "Người dùng được phép", + "allowedUsersDesc": "Mỗi email một dòng. Để trống để cho phép tất cả.", + "adminGroup": "Nhóm Admin", + "adminGroupDesc": "Người dùng trong nhóm này được cấp quyền admin. Để trống để tắt đồng bộ nhóm.", + "caCert": "Chứng chỉ CA tùy chỉnh", + "caCertDesc": "Tùy chọn. Chứng chỉ CA mã hóa PEM cho các nhà cung cấp OIDC sử dụng CA riêng hoặc tự ký. Để trống để sử dụng kho tin cậy hệ thống." + } +} diff --git a/plugins/sso/locales/translated/zh_CN.json b/plugins/sso/locales/translated/zh_CN.json new file mode 100644 index 000000000..247b4d6c4 --- /dev/null +++ b/plugins/sso/locales/translated/zh_CN.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "使用{{name}}登录", + "loading": "加载中...", + "providers": { + "docsLink": "查看文档", + "none": "未配置 SSO 提供商。", + "add": "添加提供商", + "edit": "编辑提供商", + "delete": "删除提供商", + "deleteConfirm": "删除此提供商?关联用户将无法登录。", + "name": "显示名称", + "type": "提供商类型", + "enabled": "已启用", + "save": "保存提供商", + "saving": "保存中...", + "saved": "已保存", + "saveFailed": "保存失败", + "deleted": "已删除", + "deleteFailed": "删除失败", + "cancel": "取消" + }, + "fields": { + "clientId": "客户端 ID", + "clientSecret": "客户端密钥", + "issuerUrl": "颁发者 URL", + "authUrl": "授权 URL", + "tokenUrl": "令牌 URL", + "userIdentifier": "用户标识符路径", + "displayName": "显示名称路径", + "scopes": "作用域", + "userinfoUrl": "覆盖 Userinfo URL", + "groupClaim": "组声明", + "groupClaimDesc": "可选。包含用户组的声明路径。默认依次使用 groups、roles、group。用于具有自定义声明的提供商(如 Zitadel)。", + "allowedUsers": "允许的用户", + "allowedUsersDesc": "每行一个邮箱。留空表示允许所有用户。", + "adminGroup": "管理员组", + "adminGroupDesc": "此组的用户将被授予管理员权限。留空可禁用组同步。", + "caCert": "自定义 CA 证书", + "caCertDesc": "可选。PEM 编码的 CA 证书,用于使用私有或自签名 CA 的 OIDC 提供商。留空则使用系统信任存储。" + } +} diff --git a/plugins/sso/locales/translated/zh_TW.json b/plugins/sso/locales/translated/zh_TW.json new file mode 100644 index 000000000..21971fb7a --- /dev/null +++ b/plugins/sso/locales/translated/zh_TW.json @@ -0,0 +1,41 @@ +{ + "loginWithProvider": "使用 {{name}} 登入", + "loading": "載入中...", + "providers": { + "docsLink": "檢視文件", + "none": "尚未設定 SSO 提供者。", + "add": "新增提供者", + "edit": "編輯提供者", + "delete": "刪除提供者", + "deleteConfirm": "刪除此提供者?相關使用者將無法登入。", + "name": "顯示名稱", + "type": "提供者類型", + "enabled": "已啟用", + "save": "儲存提供者", + "saving": "儲存中...", + "saved": "已儲存", + "saveFailed": "儲存失敗", + "deleted": "已刪除", + "deleteFailed": "刪除失敗", + "cancel": "取消" + }, + "fields": { + "clientId": "用戶端 ID", + "clientSecret": "用戶端密鑰", + "issuerUrl": "發行者 URL", + "authUrl": "授權 URL", + "tokenUrl": "權杖 URL", + "userIdentifier": "使用者識別碼路徑", + "displayName": "顯示名稱路徑", + "scopes": "範圍", + "userinfoUrl": "覆寫使用者資訊 URL", + "groupClaim": "群組宣告", + "groupClaimDesc": "選用。包含使用者群組的宣告路徑。預設為 groups、roles、group。用於自訂宣告的提供者(例如 Zitadel)。", + "allowedUsers": "允許的使用者", + "allowedUsersDesc": "每行一個電子郵件。留空以允許全部。", + "adminGroup": "管理員群組", + "adminGroupDesc": "此群組中的使用者將被授予管理員權限。留空以停用群組同步。", + "caCert": "自訂 CA 憑證", + "caCertDesc": "選用。PEM 編碼的 CA 憑證,用於使用私有或自簽 CA 的 OIDC 提供者。留空以使用系統信任儲存區。" + } +} diff --git a/plugins/sso/manifest.json b/plugins/sso/manifest.json new file mode 100644 index 000000000..f5548d26a --- /dev/null +++ b/plugins/sso/manifest.json @@ -0,0 +1,50 @@ +{ + "id": "sso", + "name": "Single sign-on", + "version": "1.0.0", + "description": "Sign in with an OpenID Connect provider, GitHub or Google. Each provider you add gets its own button on the login screen.", + "author": { + "name": "Termix" + }, + "license": "MIT", + "repository": "https://github.com/Termix-SSH/Termix", + "category": "Access & Security", + "icon": "KeyRound", + "engine": { + "termix": ">=2.9.0", + "api": "1" + }, + "capabilities": [ + "db:own", + "kv:own", + "secrets:own", + "network:serve", + "auth:provide", + "ui:surface" + ], + "contributes": { + "auth": { + "loginMethods": ["oidc"] + }, + "permissions": [ + { + "name": "manage", + "titleKey": "permissions.manage.title", + "descriptionKey": "permissions.manage.description", + "defaultRoles": ["admin"] + } + ], + "settings": { + "admin": [ + { + "key": "providers", + "type": "custom", + "component": "providers", + "permission": "manage" + } + ] + } + }, + "locales": "locales", + "platforms": ["linux", "win32", "darwin"] +} diff --git a/plugins/sso/migrations/mysql/0001_adopt_sso_providers.sql b/plugins/sso/migrations/mysql/0001_adopt_sso_providers.sql new file mode 100644 index 000000000..a1b185bad --- /dev/null +++ b/plugins/sso/migrations/mysql/0001_adopt_sso_providers.sql @@ -0,0 +1,14 @@ +-- sso 0001: adopt_sso_providers +-- Generated by termix-plugin migrations. Review before committing. + +CREATE TABLE IF NOT EXISTS `sso_providers` ( + `id` int AUTO_INCREMENT PRIMARY KEY, + `name` text NOT NULL, + `type` text NOT NULL, + `enabled` boolean NOT NULL DEFAULT true, + `display_order` int NOT NULL DEFAULT 0, + `config` text NOT NULL, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP) +); +RENAME TABLE `sso_providers` TO `p_sso_providers`; diff --git a/plugins/sso/migrations/mysql/0002_legacy_callback.sql b/plugins/sso/migrations/mysql/0002_legacy_callback.sql new file mode 100644 index 000000000..20e444a20 --- /dev/null +++ b/plugins/sso/migrations/mysql/0002_legacy_callback.sql @@ -0,0 +1,7 @@ +-- sso 0002: legacy_callback +-- Generated by termix-plugin migrations. Review before committing. + +ALTER TABLE `p_sso_providers` ADD COLUMN `legacy_callback` boolean NOT NULL DEFAULT false; +-- Providers that existed before 2.9 were registered with the identity +-- provider under /users/oidc/callback, so they keep sending that URI. +UPDATE `p_sso_providers` SET `legacy_callback` = true; diff --git a/plugins/sso/migrations/postgres/0001_adopt_sso_providers.sql b/plugins/sso/migrations/postgres/0001_adopt_sso_providers.sql new file mode 100644 index 000000000..0a8284f42 --- /dev/null +++ b/plugins/sso/migrations/postgres/0001_adopt_sso_providers.sql @@ -0,0 +1,14 @@ +-- sso 0001: adopt_sso_providers +-- Generated by termix-plugin migrations. Review before committing. + +CREATE TABLE IF NOT EXISTS "sso_providers" ( + "id" serial PRIMARY KEY, + "name" text NOT NULL, + "type" text NOT NULL, + "enabled" boolean NOT NULL DEFAULT true, + "display_order" integer NOT NULL DEFAULT 0, + "config" text NOT NULL, + "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP +); +ALTER TABLE "sso_providers" RENAME TO "p_sso_providers"; diff --git a/plugins/sso/migrations/postgres/0002_legacy_callback.sql b/plugins/sso/migrations/postgres/0002_legacy_callback.sql new file mode 100644 index 000000000..c0b60956a --- /dev/null +++ b/plugins/sso/migrations/postgres/0002_legacy_callback.sql @@ -0,0 +1,7 @@ +-- sso 0002: legacy_callback +-- Generated by termix-plugin migrations. Review before committing. + +ALTER TABLE "p_sso_providers" ADD COLUMN "legacy_callback" boolean NOT NULL DEFAULT false; +-- Providers that existed before 2.9 were registered with the identity +-- provider under /users/oidc/callback, so they keep sending that URI. +UPDATE "p_sso_providers" SET "legacy_callback" = true; diff --git a/plugins/sso/migrations/snapshot.json b/plugins/sso/migrations/snapshot.json new file mode 100644 index 000000000..5665de72e --- /dev/null +++ b/plugins/sso/migrations/snapshot.json @@ -0,0 +1,53 @@ +{ + "version": 1, + "tables": { + "providers": { + "columns": { + "id": { + "type": "id", + "primaryKey": true, + "notNull": true + }, + "name": { + "type": "text", + "notNull": true + }, + "type": { + "type": "text", + "notNull": true + }, + "enabled": { + "type": "boolean", + "notNull": true, + "defaultValue": true + }, + "displayOrder": { + "type": "integer", + "notNull": true, + "defaultValue": 0 + }, + "config": { + "type": "text", + "notNull": true + }, + "createdAt": { + "type": "timestamp", + "notNull": true, + "defaultNow": true + }, + "updatedAt": { + "type": "timestamp", + "notNull": true, + "defaultNow": true + }, + "legacyCallback": { + "type": "boolean", + "notNull": true, + "defaultValue": false + } + }, + "indexes": [], + "adopts": "sso_providers" + } + } +} diff --git a/plugins/sso/migrations/sqlite/0001_adopt_sso_providers.sql b/plugins/sso/migrations/sqlite/0001_adopt_sso_providers.sql new file mode 100644 index 000000000..63086970c --- /dev/null +++ b/plugins/sso/migrations/sqlite/0001_adopt_sso_providers.sql @@ -0,0 +1,14 @@ +-- sso 0001: adopt_sso_providers +-- Generated by termix-plugin migrations. Review before committing. + +CREATE TABLE IF NOT EXISTS "sso_providers" ( + "id" integer PRIMARY KEY AUTOINCREMENT, + "name" text NOT NULL, + "type" text NOT NULL, + "enabled" integer NOT NULL DEFAULT 1, + "display_order" integer NOT NULL DEFAULT 0, + "config" text NOT NULL, + "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP +); +ALTER TABLE "sso_providers" RENAME TO "p_sso_providers"; diff --git a/plugins/sso/migrations/sqlite/0002_legacy_callback.sql b/plugins/sso/migrations/sqlite/0002_legacy_callback.sql new file mode 100644 index 000000000..81b2ff5c4 --- /dev/null +++ b/plugins/sso/migrations/sqlite/0002_legacy_callback.sql @@ -0,0 +1,7 @@ +-- sso 0002: legacy_callback +-- Generated by termix-plugin migrations. Review before committing. + +ALTER TABLE "p_sso_providers" ADD COLUMN "legacy_callback" integer NOT NULL DEFAULT 0; +-- Providers that existed before 2.9 were registered with the identity +-- provider under /users/oidc/callback, so they keep sending that URI. +UPDATE "p_sso_providers" SET "legacy_callback" = 1; diff --git a/plugins/sso/package.json b/plugins/sso/package.json new file mode 100644 index 000000000..18ecb4bac --- /dev/null +++ b/plugins/sso/package.json @@ -0,0 +1,19 @@ +{ + "name": "@termix-plugin/sso", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "build": "termix-plugin build", + "test": "termix-plugin test", + "typecheck": "tsc -p tsconfig.json", + "validate": "termix-plugin validate" + }, + "dependencies": { + "jose": "^6.2.12", + "undici": "^8.10.2" + }, + "devDependencies": { + "@termix/plugin-sdk": "*" + } +} diff --git a/plugins/sso/src/backend/index.ts b/plugins/sso/src/backend/index.ts new file mode 100644 index 000000000..20a1ca322 --- /dev/null +++ b/plugins/sso/src/backend/index.ts @@ -0,0 +1,39 @@ +import type { Router } from "express"; +import type { PluginContext } from "@termix/plugin-sdk/backend"; +import { providers } from "./tables.js"; +import { createProviderStore } from "./providers.js"; +import { createSsoLogin, METHOD_ID } from "./login.js"; +import { PUBLIC_PATHS, registerSsoRoutes } from "./routes.js"; + +export async function activate(ctx: PluginContext) { + const table = await ctx.db.define(providers); + const store = createProviderStore(ctx, table); + const login = createSsoLogin(ctx, store); + + // One button per enabled provider. + ctx.auth.registerLoginMethod({ + id: METHOD_ID, + labelKey: "loginWithSso", + icon: "key-round", + kind: "redirect", + external: true, + describe: async () => + (await store.listInstances()).map((instance) => ({ + ...instance, + enabled: true, + })), + start: login.start, + callback: login.callback, + }); + + registerSsoRoutes( + ctx.http.router({ public: PUBLIC_PATHS }), + ctx, + store, + login, + ); +} + +export async function deactivate() { + // Everything above was registered through ctx and is disposed by core. +} diff --git a/plugins/sso/src/backend/login.ts b/plugins/sso/src/backend/login.ts new file mode 100644 index 000000000..fa7390b0a --- /dev/null +++ b/plugins/sso/src/backend/login.ts @@ -0,0 +1,482 @@ +/** + * The OIDC, GitHub and Google sign-in. start builds the provider's authorize + * URL; callback turns the provider's answer into a verified identity. Core + * finds or provisions the user and issues the session. + */ + +import crypto from "node:crypto"; +import { + LoginMethodError, + type PluginContext, + type PluginLoginRequest, + type PluginVerifiedIdentity, +} from "@termix/plugin-sdk/backend"; +import { + OIDCTokenFormatError, + buildFetchOptions, + extractOidcGroupsFromSources, + generatePkceCodeChallenge, + generatePkceCodeVerifier, + getDesktopCallbackUrl, + getNestedValue, + parseOidcRoleMap, + resolveOidcMappedRoles, + verifyOIDCToken, +} from "./oidc-protocol.js"; +import { + ENV_INSTANCE_ID, + ENV_PROVIDER_ID, + type ProviderStore, +} from "./providers.js"; +import type { OidcConfig, ResolvedProvider } from "./types.js"; + +export const METHOD_ID = "oidc"; +export const CALLBACK_PATH = "/plugin-api/sso/callback"; +/** The redirect URI identity providers were set up with before 2.9. */ +export const LEGACY_CALLBACK_PATH = "/users/oidc/callback"; + +const STATE_TTL_MS = 10 * 60 * 1000; +const STATE_PREFIX = "state:"; + +/** An error raised after the return address is known redirects there. */ +export class RedirectLoginError extends LoginMethodError { + constructor( + message: string, + readonly returnTo: string, + status = 400, + code?: string, + ) { + super(message, status, code); + this.name = "RedirectLoginError"; + } +} + +interface PendingState { + nonce: string; + backendCallback: string; + frontendOrigin: string; + rememberMe: boolean; + providerId: number | null; + codeVerifier: string; + createdAt: number; +} + +function nonEmpty(value: unknown): value is string { + return typeof value === "string" && value.trim().length > 0; +} + +function header(request: PluginLoginRequest, name: string): string { + const value = request.headers[name]; + return typeof (Array.isArray(value) ? value[0] : value) === "string" + ? String(Array.isArray(value) ? value[0] : value) + : ""; +} + +export function redirectUriFor( + baseUrl: string, + provider: Pick, +): string { + return `${baseUrl}${provider.legacyCallback ? LEGACY_CALLBACK_PATH : CALLBACK_PATH}`; +} + +export type SsoLogin = ReturnType; + +export function createSsoLogin(ctx: PluginContext, store: ProviderStore) { + async function saveState(state: string, pending: PendingState) { + // Drop anything abandoned so the store does not grow. + const now = Date.now(); + for (const key of await ctx.kv.list()) { + if (!key.startsWith(STATE_PREFIX)) continue; + const entry = (await ctx.kv.get(key)) as PendingState | undefined; + if (!entry || now - entry.createdAt > STATE_TTL_MS) { + await ctx.kv.delete(key); + } + } + await ctx.kv.set(`${STATE_PREFIX}${state}`, pending); + } + + async function takeState(state: string): Promise { + const key = `${STATE_PREFIX}${state}`; + const entry = (await ctx.kv.get(key)) as PendingState | undefined; + if (!entry) return null; + await ctx.kv.delete(key); + return Date.now() - entry.createdAt > STATE_TTL_MS ? null : entry; + } + + function frontendOriginFor( + request: PluginLoginRequest, + baseUrl: string, + ): string { + const { desktopCallbackPort, appCallbackUrl } = request.query; + if (desktopCallbackPort !== undefined && desktopCallbackPort !== "") { + const url = getDesktopCallbackUrl(desktopCallbackPort); + if (!url) { + throw new LoginMethodError("Invalid desktop callback port", 400); + } + return url; + } + if (typeof appCallbackUrl === "string" && appCallbackUrl) { + let callbackUrl: URL; + try { + callbackUrl = new URL(appCallbackUrl); + } catch { + throw new LoginMethodError("Invalid app callback URL", 400); + } + if (callbackUrl.protocol !== "termix-mobile:") { + throw new LoginMethodError("Unsupported app callback URL", 400); + } + return callbackUrl.toString(); + } + const referer = header(request, "referer"); + if (referer) { + try { + const refererUrl = new URL(referer); + return `${refererUrl.protocol}//${refererUrl.host}`; + } catch { + // fall through to the request's own origin + } + } + return new URL(baseUrl).origin; + } + + async function start( + request: PluginLoginRequest, + instanceId: string | null, + ): Promise<{ redirectUrl: string }> { + const requested = + instanceId ?? + (typeof request.query.providerId === "string" + ? request.query.providerId + : null); + const rowId = + requested && requested !== ENV_INSTANCE_ID + ? Number.parseInt(requested, 10) + : null; + if (rowId !== null && !Number.isInteger(rowId)) { + throw new LoginMethodError("OIDC not configured", 404); + } + const provider = await store.resolve(rowId); + if (!provider) throw new LoginMethodError("OIDC not configured", 404); + + const baseUrl = ctx.http.baseUrl(request); + const backendCallback = redirectUriFor(baseUrl, provider); + const frontendOrigin = frontendOriginFor(request, baseUrl); + const state = crypto.randomUUID(); + const nonce = crypto.randomUUID(); + const codeVerifier = generatePkceCodeVerifier(); + + await saveState(state, { + nonce, + backendCallback, + frontendOrigin, + rememberMe: String(request.query.rememberMe) === "true", + providerId: provider.rowId, + codeVerifier, + createdAt: Date.now(), + }); + + const authUrl = new URL(provider.config.authorization_url); + authUrl.searchParams.set("client_id", provider.config.client_id); + authUrl.searchParams.set("redirect_uri", backendCallback); + authUrl.searchParams.set("response_type", "code"); + authUrl.searchParams.set("scope", provider.config.scopes); + authUrl.searchParams.set("state", state); + authUrl.searchParams.set("nonce", nonce); + authUrl.searchParams.set( + "code_challenge", + generatePkceCodeChallenge(codeVerifier), + ); + authUrl.searchParams.set("code_challenge_method", "S256"); + return { redirectUrl: authUrl.toString() }; + } + + async function exchangeCode( + config: OidcConfig, + code: string, + pending: PendingState, + withVerifier: boolean, + ): Promise> { + const response = await fetch(config.token_url, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + }, + body: new URLSearchParams({ + grant_type: "authorization_code", + client_id: config.client_id, + client_secret: config.client_secret, + code, + redirect_uri: pending.backendCallback, + ...(withVerifier ? { code_verifier: pending.codeVerifier } : {}), + }), + ...buildFetchOptions(config.ca_cert), + } as RequestInit); + if (!response.ok) { + ctx.log.error( + "Token exchange failed", + new Error(`HTTP ${response.status}: ${await response.text()}`), + ); + throw new LoginMethodError("Failed to exchange authorization code", 400); + } + return (await response.json()) as Record; + } + + function providerKey(provider: ResolvedProvider): string { + return provider.rowId === null ? ENV_PROVIDER_ID : String(provider.rowId); + } + + async function githubIdentity( + provider: ResolvedProvider, + code: string, + pending: PendingState, + ): Promise { + const tokenData = await exchangeCode(provider.config, code, pending, false); + const fetchOptions = buildFetchOptions(provider.config.ca_cert); + const headers = { + Authorization: `Bearer ${tokenData.access_token}`, + Accept: "application/json", + "User-Agent": "Termix", + }; + const userInfoResponse = await fetch("https://api.github.com/user", { + headers, + ...fetchOptions, + } as RequestInit); + if (!userInfoResponse.ok) { + throw new LoginMethodError("Failed to get GitHub user information", 400); + } + const userInfo = (await userInfoResponse.json()) as Record; + + const emailResponse = await fetch("https://api.github.com/user/emails", { + headers, + }); + if (emailResponse.ok) { + const emails = (await emailResponse.json()) as Array<{ + email: string; + primary: boolean; + verified: boolean; + }>; + const primary = emails.find((e) => e.primary && e.verified); + if (primary) userInfo.email = primary.email; + } + + const subject = String(userInfo.id ?? userInfo.login); + const legacyIdentifier = `github:${provider.rowId}:${subject}`; + return { + kind: "external", + provider: providerKey(provider), + subject, + email: (userInfo.email as string | undefined) ?? null, + name: ((userInfo.name || userInfo.login) as string) || legacyIdentifier, + allowedUsers: provider.config.allowed_users ?? null, + legacy: { identifier: legacyIdentifier, providerRowId: provider.rowId }, + returnTo: pending.frontendOrigin, + rememberMe: pending.rememberMe, + }; + } + + async function fetchUserInfo( + config: OidcConfig, + accessToken: unknown, + userInfo: Record | null, + claimSources: Record[], + ): Promise | null> { + const fetchOptions = buildFetchOptions(config.ca_cert); + const issuer = config.issuer_url.endsWith("/") + ? config.issuer_url.slice(0, -1) + : config.issuer_url; + const base = issuer.replace(/\/application\/o\/[^/]+$/, ""); + const urls: string[] = []; + + try { + const discovery = await fetch( + `${issuer}/.well-known/openid-configuration`, + fetchOptions as RequestInit, + ); + if (discovery.ok) { + const document = (await discovery.json()) as Record; + if (typeof document.userinfo_endpoint === "string") { + urls.push(document.userinfo_endpoint); + } + } + } catch (error) { + ctx.log.warn(`OIDC discovery failed: ${String(error)}`); + } + + if (config.userinfo_url) urls.unshift(config.userinfo_url); + urls.push( + `${base}/userinfo/`, + `${base}/userinfo`, + `${issuer}/userinfo/`, + `${issuer}/userinfo`, + `${base}/oauth2/userinfo/`, + `${base}/oauth2/userinfo`, + `${issuer}/oauth2/userinfo/`, + `${issuer}/oauth2/userinfo`, + ); + + if (!accessToken) return userInfo; + for (const url of urls) { + try { + const response = await fetch(url, { + headers: { Authorization: `Bearer ${accessToken}` }, + ...fetchOptions, + } as RequestInit); + if (response.ok) { + const fetched = (await response.json()) as Record; + claimSources.push(fetched); + return { ...userInfo, ...fetched }; + } + ctx.log.warn(`Userinfo endpoint ${url} answered ${response.status}`); + } catch (error) { + ctx.log.warn(`Userinfo endpoint ${url} failed: ${String(error)}`); + } + } + return userInfo; + } + + async function oidcIdentity( + provider: ResolvedProvider, + code: string, + pending: PendingState, + ): Promise { + const { config } = provider; + const tokenData = await exchangeCode(config, code, pending, true); + + let userInfo: Record | null = null; + const claimSources: Record[] = []; + + if (typeof tokenData.id_token === "string") { + try { + userInfo = await verifyOIDCToken( + tokenData.id_token, + config.issuer_url, + config.client_id, + config.ca_cert, + ); + if (userInfo.nonce !== pending.nonce) { + ctx.log.warn("OIDC ID token nonce mismatch"); + throw new LoginMethodError("Invalid OIDC token nonce", 401); + } + claimSources.push(userInfo); + } catch (error) { + // A token that is not a JWS carries no claims we could trust; fall + // through to userinfo. Signature and claim failures still reject. + if (!(error instanceof OIDCTokenFormatError)) throw error; + userInfo = null; + ctx.log.warn( + `OIDC ID token cannot be verified, using userinfo: ${error.message}`, + ); + } + } + + userInfo = await fetchUserInfo( + config, + tokenData.access_token, + userInfo, + claimSources, + ); + if (!userInfo) { + throw new LoginMethodError("Failed to get user information", 400); + } + + const identifier = (getNestedValue(userInfo, config.identifier_path) || + userInfo[config.identifier_path] || + userInfo.sub || + userInfo.email || + userInfo.preferred_username) as string; + const name = (getNestedValue(userInfo, config.name_path) || + userInfo[config.name_path] || + userInfo.name || + userInfo.given_name || + identifier) as string; + if (!identifier) { + throw new LoginMethodError( + `User identifier not found at path: ${config.identifier_path}. Available fields: ${Object.keys(userInfo).join(", ")}`, + 400, + ); + } + + const groups = extractOidcGroupsFromSources( + claimSources, + config.group_claim, + ); + const isAdmin = config.admin_group + ? groups.includes(config.admin_group) + : undefined; + + let roles: { desired: string[]; managed: string[] } | undefined; + const roleMap = parseOidcRoleMap( + config.role_map ?? process.env.OIDC_ROLE_MAP, + ); + if (roleMap.size > 0) { + const { desired, managed } = resolveOidcMappedRoles(groups, roleMap); + roles = { desired: [...desired], managed: [...managed] }; + } + + return { + kind: "external", + provider: providerKey(provider), + subject: String(identifier), + email: (userInfo.email as string | undefined) ?? null, + name, + groups, + isAdmin, + allowedUsers: config.allowed_users ?? null, + roles, + logoutClaims: { + providerId: provider.rowId, + sub: typeof userInfo.sub === "string" ? userInfo.sub : null, + sid: typeof userInfo.sid === "string" ? userInfo.sid : null, + }, + legacy: { identifier: String(identifier), providerRowId: provider.rowId }, + returnTo: pending.frontendOrigin, + rememberMe: pending.rememberMe, + }; + } + + /** + * The provider's callback. Throws RedirectLoginError once the return + * address is known, and a plain LoginMethodError (answered as JSON) before. + */ + async function callback( + request: PluginLoginRequest, + ): Promise { + const source = { ...request.query, ...(request.body ?? {}) }; + const { code, state } = source as Record; + if (!nonEmpty(code) || !nonEmpty(state)) { + throw new LoginMethodError("Code and state are required", 400); + } + const pending = await takeState(state); + if (!pending) throw new LoginMethodError("Invalid state parameter", 400); + + try { + const provider = await store.resolve(pending.providerId); + if (!provider) throw new LoginMethodError("OIDC not configured", 500); + // GitHub does not issue OIDC id_tokens; it has its own token exchange. + return provider.type === "github" + ? await githubIdentity(provider, code, pending) + : await oidcIdentity(provider, code, pending); + } catch (error) { + if (error instanceof LoginMethodError) { + throw new RedirectLoginError( + error.message, + pending.frontendOrigin, + error.status, + error.code, + ); + } + ctx.log.error( + "OIDC callback failed", + error instanceof Error ? error : new Error(String(error)), + ); + throw new RedirectLoginError( + "OIDC authentication failed", + pending.frontendOrigin, + 500, + ); + } + } + + return { start, callback }; +} diff --git a/src/backend/database/routes/user-oidc-utils.ts b/plugins/sso/src/backend/oidc-protocol.ts similarity index 64% rename from src/backend/database/routes/user-oidc-utils.ts rename to plugins/sso/src/backend/oidc-protocol.ts index 8098d5db9..40e81fc34 100644 --- a/src/backend/database/routes/user-oidc-utils.ts +++ b/plugins/sso/src/backend/oidc-protocol.ts @@ -1,12 +1,12 @@ -import { authLogger } from "../../utils/logger.js"; -import type { SSOProviderType } from "../../../types/index.js"; -import { DataCrypto } from "../../utils/data-crypto.js"; -import { decryptSsoConfigSecrets } from "../../utils/system-secret-crypto.js"; +/** + * OpenID Connect protocol helpers: token verification, groups and the role + * map, back-channel logout tokens and the provider defaults. Nothing here + * touches storage. + */ + +import crypto from "node:crypto"; import { Agent } from "undici"; -import { - createCurrentSettingsRepository, - createCurrentSsoProviderRepository, -} from "../repositories/factory.js"; +import type { OidcConfig } from "./types.js"; const BACKCHANNEL_LOGOUT_EVENT = "http://schemas.openid.net/event/backchannel-logout"; @@ -22,30 +22,13 @@ export class OIDCTokenFormatError extends Error { } } -function normalizeIssuer(url: string): string { +export function normalizeIssuer(url: string): string { return url .trim() .replace(/\/+$/, "") .replace(/\/\.well-known\/openid-configuration$/, ""); } -export type OIDCConfig = { - client_id: string; - client_secret: string; - issuer_url: string; - authorization_url: string; - token_url: string; - userinfo_url: string; - identifier_path: string; - name_path: string; - scopes: string; - allowed_users: string; - admin_group: string; - group_claim?: string; - role_map?: string; - ca_cert?: string; -}; - export function buildFetchOptions(caCert?: string): Record { if (!caCert || !caCert.trim()) return {}; return { dispatcher: new Agent({ connect: { ca: caCert } }) }; @@ -71,7 +54,7 @@ export function describeFetchFailure(error: unknown): string { return cause ? `${error.message}: ${String(cause)}` : error.message; } -export function getOIDCConfigFromEnv(): OIDCConfig | null { +export function getOidcConfigFromEnv(): OidcConfig | null { const client_id = process.env.OIDC_CLIENT_ID; const client_secret = process.env.OIDC_CLIENT_SECRET; const issuer_url = process.env.OIDC_ISSUER_URL; @@ -127,7 +110,7 @@ function normalizeGroupName(group: string): string { * normalized via {@link normalizeGroupName}; role names are passed through * verbatim because they must match `roles.name` exactly. * - * Malformed entries are skipped rather than throwing — a typo in one pair must + * Malformed entries are skipped rather than throwing - a typo in one pair must * not lock every user out of login. */ export function parseOidcRoleMap(raw?: string | null): Map { @@ -157,7 +140,7 @@ export function parseOidcRoleMap(raw?: string | null): Map { * * Returns both the `desired` roles (mapped groups the user is actually in) and * the full set of `managed` roles (every role named in the map). Callers must - * only ever add/remove roles within `managed` — roles assigned by hand in + * only ever add/remove roles within `managed` - roles assigned by hand in * Termix, and the `admin`/`user` roles maintained by the admin-group sync, are * deliberately left alone. */ @@ -228,45 +211,6 @@ export function extractOidcGroupsFromSources( ]; } -export function isOIDCUserAllowed( - allowedUsers: string, - identifier: string, - email?: string, -): boolean { - if (!allowedUsers || !allowedUsers.trim()) return true; - const patterns = allowedUsers - .split(/[\n,]/) - .map((p) => p.trim()) - .filter(Boolean); - if (patterns.length === 0) return true; - - const values = [ - identifier, - ...(email && email !== identifier ? [email] : []), - ]; - for (const pattern of patterns) { - if (pattern === "*") return true; - if (pattern.includes("*")) { - const escaped = pattern - .toLowerCase() - .replace(/[.+^${}()|[\]\\]/g, "\\$&") - .replace(/\*/g, ".*"); - const regex = new RegExp(`^${escaped}$`); - if (values.some((v) => v && regex.test(v.toLowerCase()))) return true; - continue; - } - for (const value of values) { - if (!value) continue; - if (pattern.toLowerCase().startsWith("@")) { - if (value.toLowerCase().endsWith(pattern.toLowerCase())) return true; - } else { - if (value.toLowerCase() === pattern.toLowerCase()) return true; - } - } - } - return false; -} - export async function verifyOIDCToken( idToken: string, issuerUrl: string, @@ -400,10 +344,10 @@ const GITHUB_DEFAULTS = { scopes: "read:user user:email", }; -function applyProviderDefaults( - config: OIDCConfig, +export function applyProviderDefaults( + config: OidcConfig, providerType: string, -): OIDCConfig { +): OidcConfig { const defaults = providerType === "google" ? GOOGLE_DEFAULTS @@ -423,178 +367,6 @@ function applyProviderDefaults( }; } -/** - * Reads the provider secrets. System-key encrypted values are decrypted; - * values still carrying a legacy base64 prefix are decoded so login keeps - * working until the provider is next saved. - */ -async function decryptConfigSecret( - config: Record, -): Promise> { - return decryptSsoConfigSecrets(config); -} - -export async function loadProviderConfig( - providerId: number | null | undefined, - adminUserId?: string, -): Promise<{ - config: OIDCConfig; - providerType: SSOProviderType; - providerDbId: number | null; -} | null> { - const envConfig = getOIDCConfigFromEnv(); - if (envConfig && isOIDCEnvOverrideEnabled()) { - return { config: envConfig, providerType: "oidc", providerDbId: null }; - } - - if (providerId != null) { - try { - const row = - await createCurrentSsoProviderRepository().findById(providerId); - if (row) { - let parsed: Record; - try { - parsed = JSON.parse(row.config); - } catch { - parsed = {}; - } - if (adminUserId) { - try { - const adminDataKey = DataCrypto.getUserDataKey(adminUserId); - if (adminDataKey) { - parsed = DataCrypto.decryptRecord( - "settings", - parsed, - adminUserId, - adminDataKey, - ); - } - } catch { - parsed = await decryptConfigSecret(parsed); - } - } else { - parsed = await decryptConfigSecret(parsed); - } - const providerType = row.type as SSOProviderType; - const config = applyProviderDefaults( - parsed as unknown as OIDCConfig, - providerType, - ); - return { - config, - providerType, - providerDbId: row.id, - }; - } - } catch (err) { - authLogger.error("Failed to load SSO provider config by id", err, { - providerId, - }); - } - } - - // Fallback: env vars - if (envConfig) { - return { config: envConfig, providerType: "oidc", providerDbId: null }; - } - - // Fallback: first enabled OIDC-type provider in ssoProviders table - try { - const oidcRow = - await createCurrentSsoProviderRepository().findFirstEnabledOidcLike(); - if (oidcRow) { - let parsed: Record; - try { - parsed = JSON.parse(oidcRow.config); - } catch { - parsed = {}; - } - parsed = await decryptConfigSecret(parsed); - const oidcProviderType = oidcRow.type as SSOProviderType; - return { - config: applyProviderDefaults( - parsed as unknown as OIDCConfig, - oidcProviderType, - ), - providerType: oidcProviderType, - providerDbId: oidcRow.id, - }; - } - } catch { - // fall through to legacy - } - - // Fallback: legacy settings blob - try { - const legacyValue = - await createCurrentSettingsRepository().get("oidc_config"); - if (legacyValue) { - let config = JSON.parse(legacyValue) as Record; - config = await decryptConfigSecret(config); - return { - config: config as unknown as OIDCConfig, - providerType: "oidc", - providerDbId: null, - }; - } - } catch { - // no legacy config - } - - return null; -} - -export async function resolveProviderByIssuer(issuer: string): Promise<{ - config: OIDCConfig; - providerType: SSOProviderType; - providerDbId: number | null; -} | null> { - const target = normalizeIssuer(issuer); - const envConfig = getOIDCConfigFromEnv(); - if ( - envConfig?.issuer_url && - isOIDCEnvOverrideEnabled() && - normalizeIssuer(envConfig.issuer_url) === target - ) { - return { config: envConfig, providerType: "oidc", providerDbId: null }; - } - - try { - const rows = await createCurrentSsoProviderRepository().listEnabled(); - for (const row of rows) { - if (!["oidc", "github", "google"].includes(row.type)) continue; - let parsed: Record; - try { - parsed = JSON.parse(row.config); - } catch { - continue; - } - parsed = await decryptConfigSecret(parsed); - const providerType = row.type as SSOProviderType; - const config = applyProviderDefaults( - parsed as unknown as OIDCConfig, - providerType, - ); - if (config.issuer_url && normalizeIssuer(config.issuer_url) === target) { - return { config, providerType, providerDbId: row.id }; - } - } - } catch (err) { - authLogger.error("Failed to resolve SSO provider by issuer", err, { - issuer, - }); - } - - if ( - envConfig?.issuer_url && - normalizeIssuer(envConfig.issuer_url) === target - ) { - return { config: envConfig, providerType: "oidc", providerDbId: null }; - } - - return null; -} - export type LogoutTokenClaims = { sub: string | null; sid: string | null; @@ -635,7 +407,7 @@ export function validateLogoutTokenClaims( export async function validateLogoutToken( logoutToken: string, - config: OIDCConfig, + config: OidcConfig, ): Promise { const payload = await verifyOIDCToken( logoutToken, @@ -646,3 +418,46 @@ export async function validateLogoutToken( return validateLogoutTokenClaims(payload); } + +export function isValidOidcIssuer(value: unknown): boolean { + if (typeof value !== "string") return false; + try { + const url = new URL(value); + return ( + ["http:", "https:"].includes(url.protocol) && + !/\/userinfo\/?$/i.test(url.pathname) + ); + } catch { + return false; + } +} + +export function getNestedValue( + obj: Record, + path: string, +): unknown { + if (!path || !obj) return null; + return path + .split(".") + .reduce( + (current, key) => (current as Record)?.[key], + obj, + ); +} + +// RFC 7636 PKCE: 43-128 char unreserved-character string. +export function generatePkceCodeVerifier(): string { + return crypto.randomBytes(64).toString("base64url"); +} + +export function generatePkceCodeChallenge(verifier: string): string { + return crypto.createHash("sha256").update(verifier).digest("base64url"); +} + +/** The desktop app's loopback listener for a callback port it sent. */ +export function getDesktopCallbackUrl(value: unknown): string | null { + if (typeof value !== "string" || !/^\d+$/.test(value)) return null; + const port = Number(value); + if (!Number.isInteger(port) || port < 1 || port > 65535) return null; + return `http://localhost:${port}/oidc-callback`; +} diff --git a/plugins/sso/src/backend/providers.ts b/plugins/sso/src/backend/providers.ts new file mode 100644 index 000000000..8221099bb --- /dev/null +++ b/plugins/sso/src/backend/providers.ts @@ -0,0 +1,278 @@ +import { and, asc, desc, eq } from "drizzle-orm"; +import type { PluginContext } from "@termix/plugin-sdk/backend"; +import { applyProviderDefaults, normalizeIssuer } from "./oidc-protocol.js"; +import { + SSO_PROVIDER_TYPES, + type OidcConfig, + type ProviderRow, + type ResolvedProvider, + type SsoProviderType, +} from "./types.js"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +// ctx.db.define hands the table back untyped and the drizzle handle is the +// server's own, so both are typed at this module's edge. +type Table = any; +type Drizzle = any; +/* eslint-enable @typescript-eslint/no-explicit-any */ + +/** Identities from the env-configured provider use this provider id. */ +export const ENV_PROVIDER_ID = "legacy-oidc"; +/** The login instance id of the env-configured provider. */ +export const ENV_INSTANCE_ID = "0"; + +const SECRET_FIELDS = ["client_secret"] as const; +const LEGACY_PREFIXES = ["encoded:", "encrypted:"]; + +export function isSsoType(type: unknown): type is SsoProviderType { + return SSO_PROVIDER_TYPES.includes(type as SsoProviderType); +} + +export function getOidcConfigFromEnv(): OidcConfig | null { + const client_id = process.env.OIDC_CLIENT_ID; + const client_secret = process.env.OIDC_CLIENT_SECRET; + const issuer_url = process.env.OIDC_ISSUER_URL; + const authorization_url = process.env.OIDC_AUTHORIZATION_URL; + const token_url = process.env.OIDC_TOKEN_URL; + if ( + !client_id || + !client_secret || + !issuer_url || + !authorization_url || + !token_url + ) { + return null; + } + return { + client_id, + client_secret, + issuer_url, + authorization_url, + token_url, + userinfo_url: process.env.OIDC_USERINFO_URL || "", + identifier_path: process.env.OIDC_IDENTIFIER_PATH || "sub", + name_path: process.env.OIDC_NAME_PATH || "name", + scopes: process.env.OIDC_SCOPES || "openid email profile", + allowed_users: process.env.OIDC_ALLOWED_USERS || "", + admin_group: process.env.OIDC_ADMIN_GROUP || "", + group_claim: process.env.OIDC_GROUP_CLAIM || "", + role_map: process.env.OIDC_ROLE_MAP || "", + }; +} + +export function isEnvOverrideEnabled(): boolean { + return process.env.OIDC_ENV_OVERRIDE?.toLowerCase() === "true"; +} + +function envProvider(config: OidcConfig): ResolvedProvider { + // Env providers were set up against the 2.8 callback and cannot be edited. + return { config, type: "oidc", rowId: null, legacyCallback: true }; +} + +export type ProviderStore = ReturnType; + +export function createProviderStore(ctx: PluginContext, table: Table) { + const client = () => ctx.db.client(); + + /** Opens a stored secret: sealed, or the 2.8 base64 forms. */ + async function openSecret(value: string): Promise { + const prefix = LEGACY_PREFIXES.find((p) => value.startsWith(p)); + if (prefix) { + try { + return Buffer.from(value.slice(prefix.length), "base64").toString( + "utf8", + ); + } catch { + return ""; + } + } + const opened = await ctx.secrets.unseal(value); + // A value seal() never wrote was stored in the clear by hand. + return opened ?? value; + } + + async function parseConfig(raw: string): Promise> { + let config: Record; + try { + config = JSON.parse(raw); + } catch { + return {}; + } + for (const field of SECRET_FIELDS) { + const value = config[field]; + if (typeof value === "string" && value) { + config[field] = await openSecret(value); + } + } + return config; + } + + async function sealConfig(config: Record): Promise { + const out = { ...config }; + for (const field of SECRET_FIELDS) { + const value = out[field]; + if (typeof value === "string" && value) { + out[field] = await ctx.secrets.seal(value); + } + } + return JSON.stringify(out); + } + + async function listRows(): Promise { + const drizzle = await client(); + const rows = (await drizzle + .select() + .from(table) + .orderBy(asc(table.displayOrder), asc(table.id))) as ProviderRow[]; + return rows.filter((row) => isSsoType(row.type)); + } + + async function findRow(id: number): Promise { + const drizzle = await client(); + const rows = (await drizzle + .select() + .from(table) + .where(eq(table.id, id)) + .limit(1)) as ProviderRow[]; + const row = rows[0]; + return row && isSsoType(row.type) ? row : null; + } + + async function resolveRow(row: ProviderRow): Promise { + const type = row.type as SsoProviderType; + const config = applyProviderDefaults( + (await parseConfig(row.config)) as unknown as OidcConfig, + type, + ); + return { + config, + type, + rowId: row.id, + legacyCallback: !!row.legacyCallback, + }; + } + + return { + parseConfig, + sealConfig, + listRows, + findRow, + + /** Login instances: env override, rows, or the env provider alone. */ + async listInstances(): Promise< + Array<{ id: string; label: string; type: SsoProviderType }> + > { + const env = getOidcConfigFromEnv(); + const envInstance = { + id: ENV_INSTANCE_ID, + label: "SSO", + type: "oidc" as const, + }; + if (env && isEnvOverrideEnabled()) return [envInstance]; + const rows = (await listRows()).filter((row) => row.enabled); + const instances = rows.map((row) => ({ + id: String(row.id), + label: row.name, + type: row.type as SsoProviderType, + })); + if (instances.length === 0 && env) instances.push(envInstance); + return instances; + }, + + /** + * The provider for a login. With no id, the env provider or the first + * enabled row, as 2.8 did. + */ + async resolve(rowId: number | null): Promise { + const env = getOidcConfigFromEnv(); + if (env && isEnvOverrideEnabled()) return envProvider(env); + if (rowId != null) { + const row = await findRow(rowId); + return row?.enabled ? resolveRow(row) : null; + } + if (env) return envProvider(env); + const first = (await listRows()).find((row) => row.enabled); + return first ? resolveRow(first) : null; + }, + + /** The provider a back-channel logout token's issuer belongs to. */ + async resolveByIssuer(issuer: string): Promise { + const target = normalizeIssuer(issuer); + const env = getOidcConfigFromEnv(); + const envMatches = + !!env?.issuer_url && normalizeIssuer(env.issuer_url) === target; + if (envMatches && isEnvOverrideEnabled()) return envProvider(env!); + for (const row of await listRows()) { + if (!row.enabled) continue; + const resolved = await resolveRow(row); + if ( + resolved.config.issuer_url && + normalizeIssuer(resolved.config.issuer_url) === target + ) { + return resolved; + } + } + return envMatches ? envProvider(env!) : null; + }, + + async create(input: { + name: string; + type: SsoProviderType; + enabled: boolean; + displayOrder: number; + config: Record; + }): Promise { + const drizzle = await client(); + const now = new Date().toISOString(); + await drizzle.insert(table).values({ + name: input.name, + type: input.type, + enabled: input.enabled, + displayOrder: input.displayOrder, + config: await sealConfig(input.config), + legacyCallback: false, + createdAt: now, + updatedAt: now, + }); + await ctx.db.persist(); + // No returning() on every engine: find the row by what was written. + const rows = (await drizzle + .select() + .from(table) + .where(and(eq(table.name, input.name), eq(table.createdAt, now))) + .orderBy(desc(table.id)) + .limit(1)) as ProviderRow[]; + return rows[0]; + }, + + async update( + id: number, + values: Partial<{ + name: string; + enabled: boolean; + displayOrder: number; + config: Record; + legacyCallback: boolean; + }>, + ): Promise { + const drizzle = await client(); + const { config, ...rest } = values; + await drizzle + .update(table) + .set({ + ...rest, + ...(config ? { config: await sealConfig(config) } : {}), + updatedAt: new Date().toISOString(), + }) + .where(eq(table.id, id)); + await ctx.db.persist(); + return findRow(id); + }, + + async remove(id: number): Promise { + const drizzle = await client(); + await drizzle.delete(table).where(eq(table.id, id)); + await ctx.db.persist(); + }, + }; +} diff --git a/plugins/sso/src/backend/routes.ts b/plugins/sso/src/backend/routes.ts new file mode 100644 index 000000000..137d23054 --- /dev/null +++ b/plugins/sso/src/backend/routes.ts @@ -0,0 +1,502 @@ +import type { Request, Response, Router } from "express"; +import { + LoginMethodError, + type PluginContext, +} from "@termix/plugin-sdk/backend"; +import { + applyProviderDefaults, + isValidOidcIssuer, + validateLogoutToken, +} from "./oidc-protocol.js"; +import { isSsoType, type ProviderStore } from "./providers.js"; +import { METHOD_ID, redirectUriFor, type SsoLogin } from "./login.js"; +import type { OidcConfig, ProviderRow, SsoProviderType } from "./types.js"; + +export const PUBLIC_PATHS = [ + "/start", + "/callback", + "/backchannel-logout", + "/config", +]; + +const LOGOUT_JTI_TTL_MS = 5 * 60 * 1000; +const REQUIRED_OIDC_FIELDS = [ + "client_id", + "client_secret", + "issuer_url", + "authorization_url", + "token_url", +] as const; + +function fail( + ctx: PluginContext, + res: Response, + message: string, + error: unknown, +) { + ctx.log.error( + message, + error instanceof Error ? error : new Error(String(error)), + ); + res.status(500).json({ error: message }); +} + +export function registerSsoRoutes( + router: Router, + ctx: PluginContext, + store: ProviderStore, + login: SsoLogin, +): void { + // Replayed logout tokens are answered but not acted on twice. + const seenLogoutJti = new Map(); + + async function present(req: Request, row: ProviderRow) { + const config = await store.parseConfig(row.config); + const { client_secret: secret, ...rest } = config; + return { + id: row.id, + name: row.name, + type: row.type, + enabled: !!row.enabled, + displayOrder: row.displayOrder, + legacyCallback: !!row.legacyCallback, + config: rest, + hasClientSecret: typeof secret === "string" && secret.length > 0, + redirectUri: redirectUriFor(ctx.http.baseUrl(req), { + legacyCallback: !!row.legacyCallback, + }), + }; + } + + /** Checks a full provider config; returns the error to show, if any. */ + function validate(type: SsoProviderType, config: Partial) { + if (type === "oidc") { + const missing = REQUIRED_OIDC_FIELDS.filter((field) => !config[field]); + if (missing.length > 0) { + return `Missing required OIDC fields: ${missing.join(", ")}`; + } + } else if (!config.client_id || !config.client_secret) { + return "Client ID and Client Secret are required"; + } + if (config.issuer_url && !isValidOidcIssuer(config.issuer_url)) { + return "Issuer URL must be an HTTP(S) issuer and not a userinfo endpoint"; + } + return null; + } + + /** + * @openapi + * /plugin-api/sso/start: + * get: + * summary: Start an SSO login in the browser + * description: Public. Sends the browser straight to the provider, so a link can start a login. The provider is picked by the provider query parameter, or the default one. + * tags: + * - SSO + * parameters: + * - in: query + * name: provider + * schema: + * type: string + * responses: + * 302: + * description: Redirect to the identity provider. + * 404: + * description: SSO is not configured. + */ + router.get("/start", async (req: Request, res: Response) => { + try { + const instance = + typeof req.query.provider === "string" ? req.query.provider : null; + const { redirectUrl } = await login.start(req as never, instance); + res.redirect(redirectUrl); + } catch (error) { + if (error instanceof LoginMethodError) { + res.status(error.status).json({ error: error.message }); + return; + } + fail(ctx, res, "Failed to start SSO login", error); + } + }); + + /** + * @openapi + * /plugin-api/sso/callback: + * get: + * summary: SSO callback + * description: Public. The redirect URI to register with an identity provider. Exchanges the code and redirects back to the app with a session, a second-factor step or an error. + * tags: + * - SSO + * responses: + * 302: + * description: Back to the app. + * post: + * summary: SSO callback (form post) + * description: Same as the GET form, for providers that post their answer. + * tags: + * - SSO + * responses: + * 302: + * description: Back to the app. + */ + const callback = (req: Request, res: Response) => + ctx.auth.completeRedirectLogin(METHOD_ID, req, res); + router.get("/callback", callback); + router.post("/callback", callback); + + /** + * @openapi + * /plugin-api/sso/backchannel-logout: + * post: + * summary: OIDC back-channel logout + * description: Public. Where an identity provider sends a signed logout token. Ends the Termix sessions that came from the matching provider login. + * tags: + * - SSO + * requestBody: + * required: true + * content: + * application/x-www-form-urlencoded: + * schema: + * type: object + * properties: + * logout_token: + * type: string + * responses: + * 200: + * description: Logout processed. + * 400: + * description: Missing, invalid or unknown logout token. + */ + router.post("/backchannel-logout", async (req: Request, res: Response) => { + res.setHeader("Cache-Control", "no-store"); + const logoutToken = (req.body as Record | undefined) + ?.logout_token; + if (typeof logoutToken !== "string" || !logoutToken) { + res.status(400).json({ error: "missing logout_token" }); + return; + } + + let issuer: string | null = null; + try { + const parts = logoutToken.split("."); + if (parts.length === 3) { + const claims = JSON.parse(Buffer.from(parts[1], "base64").toString()); + issuer = typeof claims.iss === "string" ? claims.iss : null; + } + } catch { + issuer = null; + } + if (!issuer) { + res.status(400).json({ error: "invalid logout_token" }); + return; + } + + try { + const provider = await store.resolveByIssuer(issuer); + if (!provider) { + ctx.log.warn(`Back-channel logout for unknown issuer ${issuer}`); + res.status(400).json({ error: "unknown issuer" }); + return; + } + const claims = await validateLogoutToken(logoutToken, provider.config); + + const now = Date.now(); + for (const [key, expiry] of seenLogoutJti) { + if (expiry <= now) seenLogoutJti.delete(key); + } + if (seenLogoutJti.has(claims.jti)) { + res.status(200).json({ ok: true }); + return; + } + + try { + await ctx.auth.revokeSessions({ + providerId: provider.rowId, + sub: claims.sub, + sid: claims.sid, + }); + } catch (error) { + fail(ctx, res, "logout processing failed", error); + return; + } + seenLogoutJti.set(claims.jti, now + LOGOUT_JTI_TTL_MS); + res.status(200).json({ ok: true }); + } catch (error) { + ctx.log.warn(`Back-channel logout failed: ${String(error)}`); + res.status(400).json({ error: "invalid logout_token" }); + } + }); + + /** + * @openapi + * /plugin-api/sso/config: + * get: + * summary: Default provider's public configuration + * description: Public, for 2.8 clients. The client id, issuer, authorization URL and scopes of the default provider, or null. + * tags: + * - SSO + * responses: + * 200: + * description: Public configuration, or null. + */ + router.get("/config", async (_req: Request, res: Response) => { + try { + const provider = await store.resolve(null); + if (!provider) { + res.json(null); + return; + } + const { config } = provider; + res.json({ + client_id: config.client_id, + issuer_url: config.issuer_url, + authorization_url: config.authorization_url, + scopes: config.scopes, + }); + } catch (error) { + fail(ctx, res, "Failed to get OIDC config", error); + } + }); + + // Everything below manages providers. + const manage = ctx.rbac.require("manage") as never; + + /** + * @openapi + * /plugin-api/sso/providers: + * get: + * summary: List SSO providers + * description: Every OIDC, GitHub and Google provider with its redirect URI. Secrets are never sent, only whether one is set. Needs sso.manage. + * tags: + * - SSO + * responses: + * 200: + * description: Providers. + * 403: + * description: Missing permission. + */ + router.get("/providers", manage, async (req: Request, res: Response) => { + try { + const rows = await store.listRows(); + res.json({ + providers: await Promise.all(rows.map((row) => present(req, row))), + newRedirectUri: redirectUriFor(ctx.http.baseUrl(req), { + legacyCallback: false, + }), + }); + } catch (error) { + fail(ctx, res, "Failed to list SSO providers", error); + } + }); + + /** + * @openapi + * /plugin-api/sso/providers: + * post: + * summary: Add an SSO provider + * description: Adds an OIDC, GitHub or Google provider. GitHub and Google fill in their own endpoints. Needs sso.manage. + * tags: + * - SSO + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * name: + * type: string + * type: + * type: string + * enum: [oidc, github, google] + * enabled: + * type: boolean + * config: + * type: object + * responses: + * 201: + * description: Provider added. + * 400: + * description: Missing or invalid fields. + * 403: + * description: Missing permission. + */ + router.post("/providers", manage, async (req: Request, res: Response) => { + try { + const { + name, + type, + enabled = true, + displayOrder = 0, + config = {}, + } = req.body ?? {}; + if (typeof name !== "string" || !name.trim()) { + res.status(400).json({ error: "Provider name is required" }); + return; + } + if (!isSsoType(type)) { + res.status(400).json({ error: "Invalid provider type" }); + return; + } + const full = applyProviderDefaults(config as OidcConfig, type); + const invalid = validate(type, full); + if (invalid) { + res.status(400).json({ error: invalid }); + return; + } + const row = await store.create({ + name: name.trim(), + type, + enabled: !!enabled, + displayOrder: Number(displayOrder) || 0, + config: { ...(config as Record) }, + }); + await ctx.audit.record({ + action: "sso_provider_create", + resourceType: "sso_provider", + resourceId: String(row.id), + resourceName: row.name, + success: true, + }); + res.status(201).json(await present(req, row)); + } catch (error) { + fail(ctx, res, "Failed to create SSO provider", error); + } + }); + + /** + * @openapi + * /plugin-api/sso/providers/{id}: + * put: + * summary: Update an SSO provider + * description: Changes a provider. Config fields are merged over the stored ones; an empty client secret keeps the stored secret. legacyCallback false moves it to the new redirect URI. Needs sso.manage. + * tags: + * - SSO + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Provider updated. + * 400: + * description: Invalid fields. + * 403: + * description: Missing permission. + * 404: + * description: Provider not found. + */ + router.put("/providers/:id", manage, async (req: Request, res: Response) => { + try { + const id = Number.parseInt(String(req.params.id), 10); + const row = Number.isInteger(id) ? await store.findRow(id) : null; + if (!row) { + res.status(404).json({ error: "Provider not found" }); + return; + } + const { name, enabled, displayOrder, config, legacyCallback } = + req.body ?? {}; + const values: Parameters[1] = {}; + if (name !== undefined) { + if (typeof name !== "string" || !name.trim()) { + res.status(400).json({ error: "Provider name is required" }); + return; + } + values.name = name.trim(); + } + if (enabled !== undefined) values.enabled = !!enabled; + if (displayOrder !== undefined) { + values.displayOrder = Number(displayOrder) || 0; + } + if (legacyCallback !== undefined) { + values.legacyCallback = !!legacyCallback; + } + if (config && typeof config === "object") { + const incoming = { ...(config as Record) }; + if (!incoming.client_secret) delete incoming.client_secret; + const merged = { + ...(await store.parseConfig(row.config)), + ...incoming, + }; + const invalid = validate( + row.type as SsoProviderType, + applyProviderDefaults(merged as unknown as OidcConfig, row.type), + ); + if (invalid) { + res.status(400).json({ error: invalid }); + return; + } + values.config = merged; + } + const updated = await store.update(id, values); + await ctx.audit.record({ + action: "sso_provider_update", + resourceType: "sso_provider", + resourceId: String(id), + resourceName: updated?.name ?? row.name, + success: true, + }); + res.json(await present(req, updated ?? row)); + } catch (error) { + fail(ctx, res, "Failed to update SSO provider", error); + } + }); + + /** + * @openapi + * /plugin-api/sso/providers/{id}: + * delete: + * summary: Delete an SSO provider + * description: Refused while users still sign in with it. Needs sso.manage. + * tags: + * - SSO + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Provider deleted. + * 403: + * description: Missing permission. + * 404: + * description: Provider not found. + * 409: + * description: Users are still linked to it. + */ + router.delete( + "/providers/:id", + manage, + async (req: Request, res: Response) => { + try { + const id = Number.parseInt(String(req.params.id), 10); + const row = Number.isInteger(id) ? await store.findRow(id) : null; + if (!row) { + res.status(404).json({ error: "Provider not found" }); + return; + } + if ((await ctx.auth.countLinkedUsers(String(id))) > 0) { + res.status(409).json({ + error: + "Users still sign in with this provider. Disable it instead, or remove those users first.", + }); + return; + } + await store.remove(id); + await ctx.audit.record({ + action: "sso_provider_delete", + resourceType: "sso_provider", + resourceId: String(id), + resourceName: row.name, + success: true, + }); + res.json({ message: "SSO provider deleted" }); + } catch (error) { + fail(ctx, res, "Failed to delete SSO provider", error); + } + }, + ); +} diff --git a/plugins/sso/src/backend/tables.ts b/plugins/sso/src/backend/tables.ts new file mode 100644 index 000000000..e20156c58 --- /dev/null +++ b/plugins/sso/src/backend/tables.ts @@ -0,0 +1,32 @@ +import { + adoptLegacyTable, + boolean, + defineTable, + id, + integer, + text, + timestamp, +} from "@termix/plugin-sdk/db"; + +/** + * Adopted from core's sso_providers, so the column names are the legacy ones + * and the rename carries every provider across. LDAP rows are moved out to + * the ldap plugin by core's boot migration; until then they are ignored here. + */ +export const providers = adoptLegacyTable( + "sso_providers", + defineTable("providers", { + id: id(), + name: text().notNull(), + type: text().notNull(), + enabled: boolean().notNull().default(true), + displayOrder: integer().notNull().default(0), + config: text().notNull(), + createdAt: timestamp().notNull().defaultNow(), + updatedAt: timestamp().notNull().defaultNow(), + // Providers from before 2.9 keep sending the old redirect URI. + legacyCallback: boolean().notNull().default(false), + }), +); + +export const tables = [providers]; diff --git a/plugins/sso/src/backend/types.ts b/plugins/sso/src/backend/types.ts new file mode 100644 index 000000000..0bbdec896 --- /dev/null +++ b/plugins/sso/src/backend/types.ts @@ -0,0 +1,46 @@ +export type SsoProviderType = "oidc" | "github" | "google"; + +export const SSO_PROVIDER_TYPES: readonly SsoProviderType[] = [ + "oidc", + "github", + "google", +]; + +export interface OidcConfig { + client_id: string; + client_secret: string; + issuer_url: string; + authorization_url: string; + token_url: string; + userinfo_url: string; + identifier_path: string; + name_path: string; + scopes: string; + allowed_users: string; + admin_group: string; + group_claim?: string; + role_map?: string; + ca_cert?: string; +} + +/** A provider ready to sign someone in: secrets opened, defaults applied. */ +export interface ResolvedProvider { + config: OidcConfig; + type: SsoProviderType; + /** The row id, or null for the provider configured through OIDC_* env vars. */ + rowId: number | null; + /** Sends the 2.8 redirect URI, /users/oidc/callback, which core forwards. */ + legacyCallback: boolean; +} + +export interface ProviderRow { + id: number; + name: string; + type: string; + enabled: boolean; + displayOrder: number; + config: string; + legacyCallback: boolean; + createdAt: string; + updatedAt: string; +} diff --git a/plugins/sso/src/frontend/OidcLoginButtons.tsx b/plugins/sso/src/frontend/OidcLoginButtons.tsx new file mode 100644 index 000000000..16debd01e --- /dev/null +++ b/plugins/sso/src/frontend/OidcLoginButtons.tsx @@ -0,0 +1,39 @@ +import { useState } from "react"; +import { Button } from "@termix/plugin-sdk/ui"; +import { + useTranslation, + type LoginMethodUIProps, +} from "@termix/plugin-sdk/frontend"; + +/** One button per enabled provider. */ +export function OidcLoginButtons({ + instances, + disabled, + startRedirect, +}: LoginMethodUIProps) { + const { t } = useTranslation(); + const [loadingId, setLoadingId] = useState(null); + return ( + <> + {instances.map((instance) => ( + + ))} + + ); +} diff --git a/plugins/sso/src/frontend/ProvidersSetting.tsx b/plugins/sso/src/frontend/ProvidersSetting.tsx new file mode 100644 index 000000000..bb7005fe3 --- /dev/null +++ b/plugins/sso/src/frontend/ProvidersSetting.tsx @@ -0,0 +1,580 @@ +import { useCallback, useEffect, useState, type ReactNode } from "react"; +import { Copy, Pencil, Plus, Trash2 } from "lucide-react"; +import { toast } from "sonner"; +import { + Button, + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + Input, + Select2, + Switch, + copyToClipboard, +} from "@termix/plugin-sdk/ui"; +import { usePluginApi, useTranslation } from "@termix/plugin-sdk/frontend"; +import { + createSsoApi, + type SsoProvider, + type SsoProviderInput, + type SsoProviderType, +} from "./sso-api"; + +const TYPE_LABELS: Record = { + oidc: "OIDC", + github: "GitHub", + google: "Google", +}; + +const AUTHORIZATION_URLS: Record<"github" | "google", string> = { + github: "https://github.com/login/oauth/authorize", + google: "https://accounts.google.com/o/oauth2/v2/auth", +}; + +type Fields = { + client_id: string; + client_secret: string; + issuer_url: string; + authorization_url: string; + token_url: string; + userinfo_url: string; + identifier_path: string; + name_path: string; + scopes: string; + allowed_users: string; + admin_group: string; + group_claim: string; + ca_cert: string; +}; + +const EMPTY_FIELDS: Fields = { + client_id: "", + client_secret: "", + issuer_url: "", + authorization_url: "", + token_url: "", + userinfo_url: "", + identifier_path: "sub", + name_path: "name", + scopes: "openid email profile", + allowed_users: "", + admin_group: "", + group_claim: "", + ca_cert: "", +}; + +function errorMessage(error: unknown, fallback: string): string { + const err = error as { + response?: { data?: { error?: string } }; + message?: string; + }; + return err.response?.data?.error || err.message || fallback; +} + +const labelClass = + "text-[10px] font-semibold text-muted-foreground uppercase tracking-widest"; +const areaClass = + "w-full px-2 py-1.5 text-xs bg-background border border-border text-foreground placeholder:text-muted-foreground resize-none outline-none focus:ring-1 focus:ring-ring font-mono"; + +function Field({ + label, + required, + hint, + children, +}: { + label: string; + required?: boolean; + hint?: string; + children: ReactNode; +}) { + return ( +
+ + {hint && ( + {hint} + )} + {children} +
+ ); +} + +function RedirectUri({ uri }: { uri: string }) { + const { t } = useTranslation(); + return ( +
+ + {uri} + + +
+ ); +} + +function ProviderDialog({ + open, + onOpenChange, + provider, + newRedirectUri, + onSaved, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + provider: SsoProvider | null; + newRedirectUri: string; + onSaved: () => void; +}) { + const { t } = useTranslation(); + const api = createSsoApi(usePluginApi()); + const isEdit = provider !== null; + const [name, setName] = useState(""); + const [type, setType] = useState("oidc"); + const [enabled, setEnabled] = useState(true); + const [legacyCallback, setLegacyCallback] = useState(false); + const [fields, setFields] = useState(EMPTY_FIELDS); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (!open) return; + setName(provider?.name ?? ""); + setType(provider?.type ?? "oidc"); + setEnabled(provider?.enabled ?? true); + setLegacyCallback(provider?.legacyCallback ?? false); + const config = provider?.config ?? {}; + const next = { ...EMPTY_FIELDS }; + for (const key of Object.keys(next) as Array) { + const value = config[key]; + if (typeof value === "string") next[key] = value; + } + next.client_secret = ""; + setFields(next); + }, [open, provider]); + + const set = (key: keyof Fields) => (value: string) => + setFields((prev) => ({ ...prev, [key]: value })); + const simplified = type !== "oidc"; + + async function save() { + if (!name.trim()) { + toast.error(t("providers.nameRequired")); + return; + } + const config: Record = {}; + for (const [key, value] of Object.entries(fields)) { + if (value.trim()) config[key] = value.trim(); + } + const input: SsoProviderInput = { + name: name.trim(), + type, + enabled, + config, + ...(isEdit ? { legacyCallback } : {}), + }; + setSaving(true); + try { + if (provider) await api.update(provider.id, input); + else await api.create(input); + toast.success(t("providers.saved")); + onSaved(); + onOpenChange(false); + } catch (error) { + toast.error(errorMessage(error, t("providers.saveFailed"))); + } finally { + setSaving(false); + } + } + + const secretHint = provider?.hasClientSecret + ? t("fields.secretKeep") + : undefined; + const redirectUri = legacyCallback + ? (provider?.redirectUri ?? newRedirectUri) + : newRedirectUri; + + return ( + + + + + {isEdit ? t("providers.edit") : t("providers.add")} + + +
+ + setName(e.target.value)} + placeholder={t("providers.namePlaceholder")} + className="text-xs" + /> + + + {!isEdit && ( + + setType(e.target.value as SsoProviderType)} + className="w-full px-2 py-1.5 text-xs bg-background border border-border text-foreground outline-none focus:ring-1 focus:ring-ring" + > + {(Object.keys(TYPE_LABELS) as SsoProviderType[]).map( + (value) => ( + + ), + )} + + + )} + +
+ + +
+ +
+ + + {t("providers.redirectUriDesc")} + + + {isEdit && provider.legacyCallback && ( +
+ + {t("providers.legacyCallbackDesc", { + uri: newRedirectUri, + })} + + +
+ )} +
+ +
+ + + {t("providers.docsLink")} + + + + set("client_id")(e.target.value)} + placeholder="your-client-id" + className="text-xs" + /> + + + set("client_secret")(e.target.value)} + placeholder="your-client-secret" + className="text-xs" + /> + + {simplified ? ( + + {t("providers.authorizationUrl", { + url: AUTHORIZATION_URLS[type as "github" | "google"], + })} + + ) : ( + <> + + set("issuer_url")(e.target.value)} + placeholder="https://provider" + className="text-xs" + /> + + + set("authorization_url")(e.target.value)} + placeholder="https://provider/oauth2/auth" + className="text-xs" + /> + + + set("token_url")(e.target.value)} + placeholder="https://provider/oauth2/token" + className="text-xs" + /> + + + set("identifier_path")(e.target.value)} + placeholder="sub" + className="text-xs" + /> + + + set("name_path")(e.target.value)} + placeholder="name" + className="text-xs" + /> + + + set("scopes")(e.target.value)} + placeholder="openid email profile" + className="text-xs" + /> + + + set("userinfo_url")(e.target.value)} + placeholder="https://provider/oauth2/userinfo" + className="text-xs" + /> + + + set("group_claim")(e.target.value)} + placeholder="groups" + className="text-xs" + /> + + + )} + +