From 5c1843510515ee44d67f47c38e9b8af09a95017a Mon Sep 17 00:00:00 2001 From: LukeGus Date: Thu, 24 Sep 2026 16:57:24 -0500 Subject: [PATCH] feat: move totp and passkeys into plugins (C1) --- drizzle/mysql/0051_icy_hercules.sql | 4 + drizzle/mysql/meta/0051_snapshot.json | 5534 +++++++++++++++++ drizzle/mysql/meta/_journal.json | 7 + .../postgres/0052_motionless_krista_starr.sql | 4 + drizzle/postgres/meta/0052_snapshot.json | 5396 ++++++++++++++++ drizzle/postgres/meta/_journal.json | 7 + drizzle/sqlite/0048_cold_miek.sql | 4 + drizzle/sqlite/meta/0048_snapshot.json | 5224 ++++++++++++++++ drizzle/sqlite/meta/_journal.json | 7 + knip.json | 4 - package-lock.json | 38 +- package.json | 6 - packages/plugin-sdk/ARCHITECTURE.md | 85 +- packages/plugin-sdk/FINISH-LIST.md | 19 + packages/plugin-sdk/src/backend.ts | 8 + packages/plugin-sdk/src/frontend.ts | 14 + packages/plugin-sdk/src/testing.ts | 41 +- plugins/totp/locales/en.json | 42 + plugins/totp/locales/translated/af_ZA.json | 34 + plugins/totp/locales/translated/ar_SA.json | 34 + plugins/totp/locales/translated/bg_BG.json | 34 + plugins/totp/locales/translated/bn_BD.json | 34 + plugins/totp/locales/translated/ca_ES.json | 34 + plugins/totp/locales/translated/cs_CZ.json | 34 + plugins/totp/locales/translated/da_DK.json | 34 + plugins/totp/locales/translated/de_DE.json | 34 + plugins/totp/locales/translated/el_GR.json | 34 + plugins/totp/locales/translated/es_ES.json | 34 + plugins/totp/locales/translated/fi_FI.json | 34 + plugins/totp/locales/translated/fr_FR.json | 34 + plugins/totp/locales/translated/he_IL.json | 34 + plugins/totp/locales/translated/hi_IN.json | 34 + plugins/totp/locales/translated/hu_HU.json | 34 + plugins/totp/locales/translated/id_ID.json | 34 + plugins/totp/locales/translated/it_IT.json | 34 + plugins/totp/locales/translated/ja_JP.json | 34 + plugins/totp/locales/translated/ko_KR.json | 34 + plugins/totp/locales/translated/nl_NL.json | 34 + plugins/totp/locales/translated/no_NO.json | 34 + plugins/totp/locales/translated/pl_PL.json | 34 + plugins/totp/locales/translated/pt_BR.json | 34 + plugins/totp/locales/translated/pt_PT.json | 34 + plugins/totp/locales/translated/ro_RO.json | 34 + plugins/totp/locales/translated/ru_RU.json | 34 + plugins/totp/locales/translated/sr_SP.json | 34 + plugins/totp/locales/translated/sv_SE.json | 34 + plugins/totp/locales/translated/th_TH.json | 34 + plugins/totp/locales/translated/tr_TR.json | 34 + plugins/totp/locales/translated/uk_UA.json | 34 + plugins/totp/locales/translated/vi_VN.json | 34 + plugins/totp/locales/translated/zh_CN.json | 34 + plugins/totp/locales/translated/zh_TW.json | 34 + plugins/totp/manifest.json | 31 + .../mysql/0001_create_enrollments.sql | 12 + .../postgres/0001_create_enrollments.sql | 12 + plugins/totp/migrations/snapshot.json | 35 + .../sqlite/0001_create_enrollments.sql | 12 + plugins/totp/package.json | 21 + plugins/totp/src/backend/index.ts | 41 + plugins/totp/src/backend/repository.ts | 69 + plugins/totp/src/backend/routes.ts | 268 + plugins/totp/src/backend/tables.ts | 24 + plugins/totp/src/backend/totp.ts | 136 + plugins/totp/src/frontend/TotpChallenge.tsx | 82 + plugins/totp/src/frontend/TotpEnrollment.tsx | 404 ++ plugins/totp/src/frontend/index.tsx | 12 + plugins/totp/src/frontend/totp-input.ts | 17 + plugins/totp/tests/backend/helpers.ts | 95 + plugins/totp/tests/backend/totp.test.ts | 258 + plugins/totp/tests/frontend/activate.test.tsx | 90 + plugins/totp/tsconfig.json | 4 + plugins/totp/vitest.config.ts | 3 + plugins/webauthn/locales/en.json | 22 + .../webauthn/locales/translated/af_ZA.json | 18 + .../webauthn/locales/translated/ar_SA.json | 18 + .../webauthn/locales/translated/bg_BG.json | 18 + .../webauthn/locales/translated/bn_BD.json | 18 + .../webauthn/locales/translated/ca_ES.json | 18 + .../webauthn/locales/translated/cs_CZ.json | 18 + .../webauthn/locales/translated/da_DK.json | 18 + .../webauthn/locales/translated/de_DE.json | 18 + .../webauthn/locales/translated/el_GR.json | 18 + .../webauthn/locales/translated/es_ES.json | 18 + .../webauthn/locales/translated/fi_FI.json | 18 + .../webauthn/locales/translated/fr_FR.json | 18 + .../webauthn/locales/translated/he_IL.json | 18 + .../webauthn/locales/translated/hi_IN.json | 18 + .../webauthn/locales/translated/hu_HU.json | 18 + .../webauthn/locales/translated/id_ID.json | 18 + .../webauthn/locales/translated/it_IT.json | 18 + .../webauthn/locales/translated/ja_JP.json | 18 + .../webauthn/locales/translated/ko_KR.json | 18 + .../webauthn/locales/translated/nl_NL.json | 18 + .../webauthn/locales/translated/no_NO.json | 18 + .../webauthn/locales/translated/pl_PL.json | 18 + .../webauthn/locales/translated/pt_BR.json | 18 + .../webauthn/locales/translated/pt_PT.json | 18 + .../webauthn/locales/translated/ro_RO.json | 18 + .../webauthn/locales/translated/ru_RU.json | 18 + .../webauthn/locales/translated/sr_SP.json | 18 + .../webauthn/locales/translated/sv_SE.json | 18 + .../webauthn/locales/translated/th_TH.json | 18 + .../webauthn/locales/translated/tr_TR.json | 18 + .../webauthn/locales/translated/uk_UA.json | 18 + .../webauthn/locales/translated/vi_VN.json | 18 + .../webauthn/locales/translated/zh_CN.json | 18 + .../webauthn/locales/translated/zh_TW.json | 18 + plugins/webauthn/manifest.json | 31 + .../mysql/0001_adopt_webauthn_credentials.sql | 19 + .../0001_adopt_webauthn_credentials.sql | 19 + plugins/webauthn/migrations/snapshot.json | 65 + .../0001_adopt_webauthn_credentials.sql | 19 + plugins/webauthn/package.json | 19 + plugins/webauthn/src/backend/index.ts | 33 + plugins/webauthn/src/backend/passkeys.ts | 298 + plugins/webauthn/src/backend/repository.ts | 83 + plugins/webauthn/src/backend/routes.ts | 193 + plugins/webauthn/src/backend/tables.ts | 34 + .../src/frontend/PasskeyEnrollment.tsx | 137 + .../src/frontend/PasskeyLoginButton.tsx | 59 + plugins/webauthn/src/frontend/index.tsx | 13 + plugins/webauthn/src/frontend/webauthn-api.ts | 82 + plugins/webauthn/tests/backend/helpers.ts | 96 + .../webauthn/tests/backend/passkeys.test.ts | 371 ++ .../webauthn/tests/frontend/activate.test.tsx | 129 + plugins/webauthn/tsconfig.json | 4 + plugins/webauthn/vitest.config.ts | 3 + src/backend/auth/core-auth.ts | 24 + src/backend/auth/legacy-providers.ts | 25 +- src/backend/auth/legacy/totp-factor.ts | 128 - src/backend/auth/login-pipeline.ts | 17 +- src/backend/auth/session-issuer.ts | 10 +- src/backend/database/database.ts | 12 +- src/backend/database/db/index.ts | 41 - src/backend/database/db/schema.mysql.ts | 25 - src/backend/database/db/schema.pg.ts | 25 - src/backend/database/db/schema.ts | 25 - src/backend/database/repositories/factory.ts | 8 - .../repositories/user-auth-repository.ts | 21 + .../database/repositories/user-repository.ts | 9 - .../webauthn-credential-repository.ts | 88 - src/backend/database/routes/auth-routes.ts | 44 + .../database/routes/user-admin-routes.ts | 103 +- .../routes/user-password-reset-routes.ts | 10 +- .../database/routes/user-totp-routes.ts | 587 -- .../database/routes/user-webauthn-routes.ts | 537 -- src/backend/database/routes/users.ts | 42 +- src/backend/plugins/actor.ts | 9 +- src/backend/plugins/ctx-ssh-auth.ts | 19 +- src/backend/plugins/ctx.ts | 20 + src/backend/plugins/http.ts | 7 +- src/backend/starter.ts | 24 +- src/backend/tests/auth/auth-test-helpers.ts | 11 +- src/backend/tests/auth/legacy-methods.test.ts | 38 +- src/backend/tests/auth/login-pipeline.test.ts | 69 +- .../user-session-repositories.test.ts | 3 - .../routes/desktop-auto-session.test.ts | 6 - .../routes/totp-disable-route.test.ts | 194 - .../database/routes/user-admin-routes.test.ts | 79 +- .../routes/user-password-reset-routes.test.ts | 13 +- .../database/routes/user-totp-routes.test.ts | 82 - .../routes/user-webauthn-routes.test.ts | 179 - src/backend/tests/plugins/ctx-secrets.test.ts | 17 + .../tests/plugins/ctx-ssh-auth.test.ts | 62 +- .../external-identity-migration.test.ts | 23 +- .../tests/utils/totp-migration.test.ts | 196 + .../user-encryption-migration-store.test.ts | 21 +- src/backend/utils/auth-manager.ts | 6 +- .../external-identity-migration.ts | 30 +- .../utils/crypto-migration/totp-migration.ts | 168 + src/backend/utils/data-crypto.ts | 23 - src/backend/utils/field-crypto.ts | 8 +- src/backend/utils/lazy-field-encryption.ts | 30 - src/backend/utils/swagger.ts | 4 - .../utils/user-encryption-migration-store.ts | 30 - src/ui/api/system-status-api.ts | 89 +- src/ui/api/user-management-api.ts | 13 - src/ui/api/webauthn-api.ts | 127 - src/ui/auth/Auth.tsx | 78 +- src/ui/auth/ElectronLoginForm.tsx | 3 +- src/ui/auth/legacy-auth-ui.tsx | 163 +- src/ui/locales/en.json | 58 - src/ui/locales/translated/af_ZA.json | 56 - src/ui/locales/translated/ar_SA.json | 56 - src/ui/locales/translated/bg_BG.json | 56 - src/ui/locales/translated/bn_BD.json | 56 - src/ui/locales/translated/ca_ES.json | 56 - src/ui/locales/translated/cs_CZ.json | 56 - src/ui/locales/translated/da_DK.json | 56 - src/ui/locales/translated/de_DE.json | 56 - src/ui/locales/translated/el_GR.json | 56 - src/ui/locales/translated/es_ES.json | 56 - src/ui/locales/translated/fi_FI.json | 56 - src/ui/locales/translated/fr_FR.json | 56 - src/ui/locales/translated/he_IL.json | 56 - src/ui/locales/translated/hi_IN.json | 56 - src/ui/locales/translated/hu_HU.json | 56 - src/ui/locales/translated/id_ID.json | 56 - src/ui/locales/translated/it_IT.json | 56 - src/ui/locales/translated/ja_JP.json | 56 - src/ui/locales/translated/ko_KR.json | 56 - src/ui/locales/translated/nl_NL.json | 56 - src/ui/locales/translated/no_NO.json | 56 - src/ui/locales/translated/pl_PL.json | 56 - src/ui/locales/translated/pt_BR.json | 56 - src/ui/locales/translated/pt_PT.json | 56 - src/ui/locales/translated/ro_RO.json | 56 - src/ui/locales/translated/ru_RU.json | 56 - src/ui/locales/translated/sr_SP.json | 56 - src/ui/locales/translated/sv_SE.json | 56 - src/ui/locales/translated/th_TH.json | 56 - src/ui/locales/translated/tr_TR.json | 56 - src/ui/locales/translated/uk_UA.json | 56 - src/ui/locales/translated/vi_VN.json | 56 - src/ui/locales/translated/zh_CN.json | 56 - src/ui/locales/translated/zh_TW.json | 56 - src/ui/main-axios.ts | 9 +- src/ui/plugin-host/app.ts | 3 + src/ui/plugin-host/auth-registry.ts | 11 +- src/ui/plugin-host/testing-host.tsx | 49 + src/ui/sidebar/AdminManagementSections.tsx | 2 +- src/ui/sidebar/AdminSecondFactorsSection.tsx | 8 +- src/ui/sidebar/AdminSettingsPanel.tsx | 6 +- src/ui/sidebar/AdminUserManagePanel.tsx | 53 +- src/ui/sidebar/AuthEnrollmentSections.tsx | 32 + .../sidebar/PluginSecondFactorEnrollment.tsx | 29 - src/ui/sidebar/UserProfilePanel.tsx | 556 +- src/ui/ssh/dialogs/TOTPDialog.tsx | 7 +- src/ui/tests/api/webauthn-api.test.ts | 121 - src/ui/tests/auth/Auth.test.tsx | 1 - .../auth/AuthEmbeddedServerFailure.test.tsx | 6 - src/ui/tests/auth/AuthLoginMethods.test.tsx | 80 +- src/ui/tests/auth/ElectronLoginForm.test.tsx | 3 +- .../AdminSecondFactorsSection.test.tsx | 6 +- .../sidebar/AdminUserManagePanel.test.tsx | 19 +- 235 files changed, 22545 insertions(+), 5610 deletions(-) create mode 100644 drizzle/mysql/0051_icy_hercules.sql create mode 100644 drizzle/mysql/meta/0051_snapshot.json create mode 100644 drizzle/postgres/0052_motionless_krista_starr.sql create mode 100644 drizzle/postgres/meta/0052_snapshot.json create mode 100644 drizzle/sqlite/0048_cold_miek.sql create mode 100644 drizzle/sqlite/meta/0048_snapshot.json create mode 100644 plugins/totp/locales/en.json create mode 100644 plugins/totp/locales/translated/af_ZA.json create mode 100644 plugins/totp/locales/translated/ar_SA.json create mode 100644 plugins/totp/locales/translated/bg_BG.json create mode 100644 plugins/totp/locales/translated/bn_BD.json create mode 100644 plugins/totp/locales/translated/ca_ES.json create mode 100644 plugins/totp/locales/translated/cs_CZ.json create mode 100644 plugins/totp/locales/translated/da_DK.json create mode 100644 plugins/totp/locales/translated/de_DE.json create mode 100644 plugins/totp/locales/translated/el_GR.json create mode 100644 plugins/totp/locales/translated/es_ES.json create mode 100644 plugins/totp/locales/translated/fi_FI.json create mode 100644 plugins/totp/locales/translated/fr_FR.json create mode 100644 plugins/totp/locales/translated/he_IL.json create mode 100644 plugins/totp/locales/translated/hi_IN.json create mode 100644 plugins/totp/locales/translated/hu_HU.json create mode 100644 plugins/totp/locales/translated/id_ID.json create mode 100644 plugins/totp/locales/translated/it_IT.json create mode 100644 plugins/totp/locales/translated/ja_JP.json create mode 100644 plugins/totp/locales/translated/ko_KR.json create mode 100644 plugins/totp/locales/translated/nl_NL.json create mode 100644 plugins/totp/locales/translated/no_NO.json create mode 100644 plugins/totp/locales/translated/pl_PL.json create mode 100644 plugins/totp/locales/translated/pt_BR.json create mode 100644 plugins/totp/locales/translated/pt_PT.json create mode 100644 plugins/totp/locales/translated/ro_RO.json create mode 100644 plugins/totp/locales/translated/ru_RU.json create mode 100644 plugins/totp/locales/translated/sr_SP.json create mode 100644 plugins/totp/locales/translated/sv_SE.json create mode 100644 plugins/totp/locales/translated/th_TH.json create mode 100644 plugins/totp/locales/translated/tr_TR.json create mode 100644 plugins/totp/locales/translated/uk_UA.json create mode 100644 plugins/totp/locales/translated/vi_VN.json create mode 100644 plugins/totp/locales/translated/zh_CN.json create mode 100644 plugins/totp/locales/translated/zh_TW.json create mode 100644 plugins/totp/manifest.json create mode 100644 plugins/totp/migrations/mysql/0001_create_enrollments.sql create mode 100644 plugins/totp/migrations/postgres/0001_create_enrollments.sql create mode 100644 plugins/totp/migrations/snapshot.json create mode 100644 plugins/totp/migrations/sqlite/0001_create_enrollments.sql create mode 100644 plugins/totp/package.json create mode 100644 plugins/totp/src/backend/index.ts create mode 100644 plugins/totp/src/backend/repository.ts create mode 100644 plugins/totp/src/backend/routes.ts create mode 100644 plugins/totp/src/backend/tables.ts create mode 100644 plugins/totp/src/backend/totp.ts create mode 100644 plugins/totp/src/frontend/TotpChallenge.tsx create mode 100644 plugins/totp/src/frontend/TotpEnrollment.tsx create mode 100644 plugins/totp/src/frontend/index.tsx create mode 100644 plugins/totp/src/frontend/totp-input.ts create mode 100644 plugins/totp/tests/backend/helpers.ts create mode 100644 plugins/totp/tests/backend/totp.test.ts create mode 100644 plugins/totp/tests/frontend/activate.test.tsx create mode 100644 plugins/totp/tsconfig.json create mode 100644 plugins/totp/vitest.config.ts create mode 100644 plugins/webauthn/locales/en.json create mode 100644 plugins/webauthn/locales/translated/af_ZA.json create mode 100644 plugins/webauthn/locales/translated/ar_SA.json create mode 100644 plugins/webauthn/locales/translated/bg_BG.json create mode 100644 plugins/webauthn/locales/translated/bn_BD.json create mode 100644 plugins/webauthn/locales/translated/ca_ES.json create mode 100644 plugins/webauthn/locales/translated/cs_CZ.json create mode 100644 plugins/webauthn/locales/translated/da_DK.json create mode 100644 plugins/webauthn/locales/translated/de_DE.json create mode 100644 plugins/webauthn/locales/translated/el_GR.json create mode 100644 plugins/webauthn/locales/translated/es_ES.json create mode 100644 plugins/webauthn/locales/translated/fi_FI.json create mode 100644 plugins/webauthn/locales/translated/fr_FR.json create mode 100644 plugins/webauthn/locales/translated/he_IL.json create mode 100644 plugins/webauthn/locales/translated/hi_IN.json create mode 100644 plugins/webauthn/locales/translated/hu_HU.json create mode 100644 plugins/webauthn/locales/translated/id_ID.json create mode 100644 plugins/webauthn/locales/translated/it_IT.json create mode 100644 plugins/webauthn/locales/translated/ja_JP.json create mode 100644 plugins/webauthn/locales/translated/ko_KR.json create mode 100644 plugins/webauthn/locales/translated/nl_NL.json create mode 100644 plugins/webauthn/locales/translated/no_NO.json create mode 100644 plugins/webauthn/locales/translated/pl_PL.json create mode 100644 plugins/webauthn/locales/translated/pt_BR.json create mode 100644 plugins/webauthn/locales/translated/pt_PT.json create mode 100644 plugins/webauthn/locales/translated/ro_RO.json create mode 100644 plugins/webauthn/locales/translated/ru_RU.json create mode 100644 plugins/webauthn/locales/translated/sr_SP.json create mode 100644 plugins/webauthn/locales/translated/sv_SE.json create mode 100644 plugins/webauthn/locales/translated/th_TH.json create mode 100644 plugins/webauthn/locales/translated/tr_TR.json create mode 100644 plugins/webauthn/locales/translated/uk_UA.json create mode 100644 plugins/webauthn/locales/translated/vi_VN.json create mode 100644 plugins/webauthn/locales/translated/zh_CN.json create mode 100644 plugins/webauthn/locales/translated/zh_TW.json create mode 100644 plugins/webauthn/manifest.json create mode 100644 plugins/webauthn/migrations/mysql/0001_adopt_webauthn_credentials.sql create mode 100644 plugins/webauthn/migrations/postgres/0001_adopt_webauthn_credentials.sql create mode 100644 plugins/webauthn/migrations/snapshot.json create mode 100644 plugins/webauthn/migrations/sqlite/0001_adopt_webauthn_credentials.sql create mode 100644 plugins/webauthn/package.json create mode 100644 plugins/webauthn/src/backend/index.ts create mode 100644 plugins/webauthn/src/backend/passkeys.ts create mode 100644 plugins/webauthn/src/backend/repository.ts create mode 100644 plugins/webauthn/src/backend/routes.ts create mode 100644 plugins/webauthn/src/backend/tables.ts create mode 100644 plugins/webauthn/src/frontend/PasskeyEnrollment.tsx create mode 100644 plugins/webauthn/src/frontend/PasskeyLoginButton.tsx create mode 100644 plugins/webauthn/src/frontend/index.tsx create mode 100644 plugins/webauthn/src/frontend/webauthn-api.ts create mode 100644 plugins/webauthn/tests/backend/helpers.ts create mode 100644 plugins/webauthn/tests/backend/passkeys.test.ts create mode 100644 plugins/webauthn/tests/frontend/activate.test.tsx create mode 100644 plugins/webauthn/tsconfig.json create mode 100644 plugins/webauthn/vitest.config.ts delete mode 100644 src/backend/auth/legacy/totp-factor.ts delete mode 100644 src/backend/database/repositories/webauthn-credential-repository.ts delete mode 100644 src/backend/database/routes/user-totp-routes.ts delete mode 100644 src/backend/database/routes/user-webauthn-routes.ts delete mode 100644 src/backend/tests/database/routes/totp-disable-route.test.ts delete mode 100644 src/backend/tests/database/routes/user-totp-routes.test.ts delete mode 100644 src/backend/tests/database/routes/user-webauthn-routes.test.ts create mode 100644 src/backend/tests/utils/totp-migration.test.ts create mode 100644 src/backend/utils/crypto-migration/totp-migration.ts delete mode 100644 src/ui/api/webauthn-api.ts create mode 100644 src/ui/sidebar/AuthEnrollmentSections.tsx delete mode 100644 src/ui/sidebar/PluginSecondFactorEnrollment.tsx delete mode 100644 src/ui/tests/api/webauthn-api.test.ts diff --git a/drizzle/mysql/0051_icy_hercules.sql b/drizzle/mysql/0051_icy_hercules.sql new file mode 100644 index 000000000..b8213f380 --- /dev/null +++ b/drizzle/mysql/0051_icy_hercules.sql @@ -0,0 +1,4 @@ +-- webauthn_credentials moves to the webauthn plugin's adopted table at +-- activation, and the users TOTP columns are read by the TOTP boot migration +-- after this runs. Dropping either here would lose the data first. +SELECT 1; diff --git a/drizzle/mysql/meta/0051_snapshot.json b/drizzle/mysql/meta/0051_snapshot.json new file mode 100644 index 000000000..1565313de --- /dev/null +++ b/drizzle/mysql/meta/0051_snapshot.json @@ -0,0 +1,5534 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "8475e76e-38d8-47eb-b6a8-c4eb686e0a6f", + "prevId": "d1df4ee5-2229-4147-9097-084f17740dc3", + "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": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "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": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sso_providers_id": { + "name": "sso_providers_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "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 a58cf422e..1a6acfbe1 100644 --- a/drizzle/mysql/meta/_journal.json +++ b/drizzle/mysql/meta/_journal.json @@ -358,6 +358,13 @@ "when": 1790284032088, "tag": "0050_optimal_speedball", "breakpoints": true + }, + { + "idx": 51, + "version": "5", + "when": 1790285910582, + "tag": "0051_icy_hercules", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/postgres/0052_motionless_krista_starr.sql b/drizzle/postgres/0052_motionless_krista_starr.sql new file mode 100644 index 000000000..b8213f380 --- /dev/null +++ b/drizzle/postgres/0052_motionless_krista_starr.sql @@ -0,0 +1,4 @@ +-- webauthn_credentials moves to the webauthn plugin's adopted table at +-- activation, and the users TOTP columns are read by the TOTP boot migration +-- after this runs. Dropping either here would lose the data first. +SELECT 1; diff --git a/drizzle/postgres/meta/0052_snapshot.json b/drizzle/postgres/meta/0052_snapshot.json new file mode 100644 index 000000000..03bda9ac6 --- /dev/null +++ b/drizzle/postgres/meta/0052_snapshot.json @@ -0,0 +1,5396 @@ +{ + "id": "57c14e5d-ae42-436c-8317-020b807969e4", + "prevId": "05fbe41e-121b-439e-a612-fe23587693af", + "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.sso_providers": { + "name": "sso_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "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": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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 d92e5be56..b035e2ac7 100644 --- a/drizzle/postgres/meta/_journal.json +++ b/drizzle/postgres/meta/_journal.json @@ -365,6 +365,13 @@ "when": 1790284031484, "tag": "0051_useful_slyde", "breakpoints": true + }, + { + "idx": 52, + "version": "7", + "when": 1790285909968, + "tag": "0052_motionless_krista_starr", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/sqlite/0048_cold_miek.sql b/drizzle/sqlite/0048_cold_miek.sql new file mode 100644 index 000000000..b8213f380 --- /dev/null +++ b/drizzle/sqlite/0048_cold_miek.sql @@ -0,0 +1,4 @@ +-- webauthn_credentials moves to the webauthn plugin's adopted table at +-- activation, and the users TOTP columns are read by the TOTP boot migration +-- after this runs. Dropping either here would lose the data first. +SELECT 1; diff --git a/drizzle/sqlite/meta/0048_snapshot.json b/drizzle/sqlite/meta/0048_snapshot.json new file mode 100644 index 000000000..409b505a7 --- /dev/null +++ b/drizzle/sqlite/meta/0048_snapshot.json @@ -0,0 +1,5224 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "cda293ef-98c8-4572-98c7-70c8bd244c40", + "prevId": "2519d870-63a7-4aef-a424-aa7f2c4dc7f9", + "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": {} + }, + "sso_providers": { + "name": "sso_providers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "display_order": { + "name": "display_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "config": { + "name": "config", + "type": "text", + "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": {}, + "foreignKeys": {}, + "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 bdd46123c..652c01341 100644 --- a/drizzle/sqlite/meta/_journal.json +++ b/drizzle/sqlite/meta/_journal.json @@ -337,6 +337,13 @@ "when": 1790284030834, "tag": "0047_futuristic_microbe", "breakpoints": true + }, + { + "idx": 48, + "version": "6", + "when": 1790285909308, + "tag": "0048_cold_miek", + "breakpoints": true } ] } \ No newline at end of file diff --git a/knip.json b/knip.json index c6669a435..b0f3b4798 100644 --- a/knip.json +++ b/knip.json @@ -39,8 +39,6 @@ "@types/js-yaml", "@types/jsonwebtoken", "@types/multer", - "@types/qrcode", - "@types/speakeasy", "@types/ssh2", "@uiw/codemirror-extensions-langs", "@uiw/codemirror-theme-github", @@ -71,7 +69,6 @@ "jszip", "multer", "nanoid", - "qrcode", "i18next-browser-languagedetector", "lucide-react", "motion", @@ -87,7 +84,6 @@ "react-xtermjs", "remark-gfm", "socks", - "speakeasy", "ssh2", "undici", "husky", diff --git a/package-lock.json b/package-lock.json index 81aa5f582..adfa7351c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,8 +14,6 @@ ], "dependencies": { "@anthropic-ai/sdk": "^0.120.0", - "@simplewebauthn/browser": "^13.3.0", - "@simplewebauthn/server": "^13.3.2", "@tanstack/react-virtual": "^3.14.13", "@types/compression": "^1.8.1", "@types/ldapjs": "^3.0.6", @@ -43,12 +41,10 @@ "nanoid": "^6.0.1", "node-pty": "^1.1.0", "pg": "^8.23.0", - "qrcode": "^1.5.4", "redis": "^6.2.1", "semver": "^7.8.5", "sharp": "^0.35.4", "socks": "^2.8.10", - "speakeasy": "^2.0.0", "ssh2": "^1.17.0", "undici": "^8.10.2", "ws": "^8.21.3" @@ -101,10 +97,8 @@ "@types/multer": "^2.2.0", "@types/node": "^26.2.0", "@types/pg": "^8.23.1", - "@types/qrcode": "^1.5.6", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", - "@types/speakeasy": "^2.0.10", "@types/ssh2": "^1.15.6", "@types/ws": "^8.18.1", "@uiw/codemirror-extensions-langs": "^4.25.11", @@ -7048,6 +7042,10 @@ "resolved": "plugins/tmux-monitor", "link": true }, + "node_modules/@termix-plugin/totp": { + "resolved": "plugins/totp", + "link": true + }, "node_modules/@termix-plugin/tunnels": { "resolved": "plugins/tunnels", "link": true @@ -7060,6 +7058,10 @@ "resolved": "plugins/web-endpoint", "link": true }, + "node_modules/@termix-plugin/webauthn": { + "resolved": "plugins/webauthn", + "link": true + }, "node_modules/@termix-plugin/workspaces": { "resolved": "plugins/workspaces", "link": true @@ -18692,6 +18694,19 @@ "@termix/plugin-sdk": "*" } }, + "plugins/totp": { + "name": "@termix-plugin/totp", + "version": "1.0.0", + "dependencies": { + "qrcode": "^1.5.4", + "speakeasy": "^2.0.0" + }, + "devDependencies": { + "@termix/plugin-sdk": "*", + "@types/qrcode": "^1.5.6", + "@types/speakeasy": "^2.0.10" + } + }, "plugins/tunnels": { "name": "@termix-plugin/tunnels", "version": "1.0.0", @@ -18713,6 +18728,17 @@ "@termix/plugin-sdk": "*" } }, + "plugins/webauthn": { + "name": "@termix-plugin/webauthn", + "version": "1.0.0", + "dependencies": { + "@simplewebauthn/browser": "^13.3.0", + "@simplewebauthn/server": "^13.3.2" + }, + "devDependencies": { + "@termix/plugin-sdk": "*" + } + }, "plugins/workspaces": { "name": "@termix-plugin/workspaces", "version": "1.0.0", diff --git a/package.json b/package.json index aa0cb4e77..ca782a4b2 100644 --- a/package.json +++ b/package.json @@ -56,8 +56,6 @@ }, "dependencies": { "@anthropic-ai/sdk": "^0.120.0", - "@simplewebauthn/browser": "^13.3.0", - "@simplewebauthn/server": "^13.3.2", "@tanstack/react-virtual": "^3.14.13", "@types/compression": "^1.8.1", "@types/ldapjs": "^3.0.6", @@ -85,12 +83,10 @@ "nanoid": "^6.0.1", "node-pty": "^1.1.0", "pg": "^8.23.0", - "qrcode": "^1.5.4", "redis": "^6.2.1", "semver": "^7.8.5", "sharp": "^0.35.4", "socks": "^2.8.10", - "speakeasy": "^2.0.0", "ssh2": "^1.17.0", "undici": "^8.10.2", "ws": "^8.21.3" @@ -143,10 +139,8 @@ "@types/multer": "^2.2.0", "@types/node": "^26.2.0", "@types/pg": "^8.23.1", - "@types/qrcode": "^1.5.6", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", - "@types/speakeasy": "^2.0.10", "@types/ssh2": "^1.15.6", "@types/ws": "^8.18.1", "@uiw/codemirror-extensions-langs": "^4.25.11", diff --git a/packages/plugin-sdk/ARCHITECTURE.md b/packages/plugin-sdk/ARCHITECTURE.md index 0cd5bceea..ed138eed5 100644 --- a/packages/plugin-sdk/ARCHITECTURE.md +++ b/packages/plugin-sdk/ARCHITECTURE.md @@ -454,7 +454,7 @@ boundary and Suspense. | `registerAction`, `declareActionSlot` | Frontend actions and the slots a plugin owns | | `registerSlotContribution`, `invokeAction` | Fill a slot with a `button` or a `component`, with an optional `when`; call an action and get its result | | `registerSshAuthEditor` | An SSH auth method's editor in the host editor | -| `registerLoginMethod`, `registerSecondFactorUI` | Login screen UI for a method and a second factor challenge (plus an optional `enrollment` section) | +| `registerLoginMethod`, `registerSecondFactorUI` | Login screen UI for a method (`placement: "inline"` beside the password form) and a second factor challenge, each with an optional `enrollment` | | `api`, `wsUrl(path)` | axios on `/plugin-api//` and the plugin's WebSocket URL | | `apiFor(origin)` | The same client for a resolved connection origin: `"remote"` reaches the desktop app's connected server (**B14**) | | `fetch(path, init)` | A raw `fetch` on `/plugin-api//` with core's auth, for a streamed response (SSE) axios cannot read (**B18**) | @@ -575,7 +575,9 @@ popover, scroll-area, select, select2, separator, skeleton, switch, textarea, tooltip, sheet, section-card, metric-card, charts, the card grid, `ConnectionScreen` and the connection status helpers, `SnippetVariablesDialog`, `FullScreenAppWrapper`, the connection log context, -`TOTPDialog`, `SSHAuthDialog`, `WarpgateDialog`, `PassphraseDialog`, +`TOTPDialog` (its `MFAPromptMode` type lives in +`@termix/plugin-sdk/frontend` since **C1**, so the shell spells no `"totp"`), +`SSHAuthDialog`, `WarpgateDialog`, `PassphraseDialog`, `OPKSSHDialog` (until opkssh moves in Phase C), `HostKeyVerificationDialog`, `useTabs`/`useTabsSafe`, `ActionSlot`, `ComponentSlot`, `PluginComponent` and `FOLDER_COLORS` (the colour swatches folders and workspaces pick from). @@ -980,7 +982,11 @@ and activates it again that way. `layout` (what `app.tabs.getLayout()` returns) and `ready` (fire `app.tabs.onReady`). `app.tabs.applyLayout` is recorded in `shellCalls` and runs the shell's real restore rules, `openedTabs()` lists what it opened, and -`renderOpenedTab(i)` renders one the way the shell would, placeholder included. `renderWithApp` activates the plugin against core's real registries, +`renderOpenedTab(i)` renders one the way the shell would, placeholder included. +**C1** added `registered.loginMethods()` / `.secondFactors()` and +`renderLoginMethod`, `renderSecondFactor` and `renderEnrollment`, and the +backend doubles take `refuseEnrollment` to make `recordEnrollment` refuse the +way core's policy does. `renderWithApp` activates the plugin against core's real registries, records what it registered and the shell calls it made, renders any tab, panel, card, host editor section, settings component or slot, and `deactivate()` disposes it all. Every bundled plugin has a @@ -993,13 +999,50 @@ 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, passkeys, TOTP, 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 +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. Nothing else in core branches on those type names. +**C1** moved TOTP and passkeys out: + +- `totp` registers the `totp` second factor. It owns `p_totp_enrollments` + (one row per user: the active secret, an unfinished setup's secret and the + backup codes, all sealed with `ctx.secrets.seal`), so `verify` reads them + during login with no acting user. Its routes at `/plugin-api/totp/` + (`status`, `setup`, `enable`, `disable`, `backup-codes`) re-authenticate + with a TOTP or backup code, never the account password, which a plugin + cannot see. `enable` calls `ctx.auth.recordEnrollment` before it turns + anything on, so core's policy can refuse it cleanly. +- `webauthn` registers the `passkey` login method (inline on the login + screen) and adopts `webauthn_credentials`. A passkey is a login method only, + as in 2.8: an assertion whose authenticator verified the user returns + `mfaSatisfied`, which skips core's second-factor step; without it the user + still gets their factors. The relying party id and origin come from the + request exactly as before (no core setting holds them); the relying party + name is `ctx.settings.readCore("app_name")`. + `POST /plugin-api/webauthn/authenticate/options` is public; the assertion + goes to core's `/users/auth/passkey/verify`. +- `src/backend/utils/crypto-migration/totp-migration.ts` takes a 2.8 + database across: every `core`/`totp` enrolment row (A8) becomes + `totp`/`totp`, every user with `users.totp_enabled` gets one, and once + the plugin's table exists each secret and its backup codes are decrypted + with the user's data key, sealed into it, and the `users` columns cleared. + The rows move even while the plugin is off, so those users fail closed. The + `users` TOTP columns left `schema.ts`, and the drizzle migration that would + drop them is a no-op, like an adopted table. +- Core keeps no TOTP knowledge. `totp_enabled` in the login body and + `/users/me` is still sent under that name for 2.8 clients (Termix-Mobile, + older remote-sync peers) but means "has any second factor"; the admin user + list sends `second_factor_enabled`. Trusted proxy login, "disable password + login" and a password reset that wipes data go through + `user_second_factors` and `resetUserSecondFactors`. + `/users/admin/totp/disable` is gone; the generic admin reset covers it. + `AuthManager.unlockWithSystemKey` (was `authenticateWebAuthnUser`) opens + the data key for any identity that carries no password. + #### One SSH connect pipeline Every SSH connection, core's and plugins', goes through @@ -1130,8 +1173,18 @@ Routes, all under `/users/auth`: `GET methods` (public), `GET :methodId/start`, `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`, -`/users/totp/verify-login`, `/users/oidc/*`, `/users/ldap/login`, -`/users/webauthn/authenticate/verify`) are thin wrappers over the same pipeline. +`/users/oidc/*`, `/users/ldap/login`) are thin wrappers over the same +pipeline. `/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. + +**Enrolment policy (C1).** `ctx.auth.recordEnrollment` refuses with a +`LoginMethodError` (409) while trusted proxy login is on or the password +login setting is off, the rules the 2.8 TOTP route had, now applied to every +factor. When it records one it also revokes the user's other sessions and +forgets their trusted devices, so nothing that skipped the factor before +keeps skipping it. The session to keep is the request's: the HTTP middleware +puts the session id next to the actor (`getActorSessionId`). An admin clears a user's factors, including orphaned ones, with `DELETE /users/admin/:userId/second-factors` (audited as @@ -1152,7 +1205,11 @@ lists (props: `instances`, `rememberMe`, `submit`, `startRedirect`, `complete`); a redirect method with no UI gets one button per instance. `app.registerSecondFactorUI` draws the challenge (props: `verify`, `challenge`, `cancel`) and, with `enrollment`, a section in Settings > -Security. At activation before sign-in `app.api` calls fail with 401, so do +Security. **C1** gave `app.registerLoginMethod` the same `enrollment` (a +passkey is registered there) and `placement: "inline"`, which also draws +the method under the password form; the login screen no longer knows the +passkey method by name. A server that sends a second-factor step without +naming its factors gets every factor UI registered on the screen. At activation before sign-in `app.api` calls fail with 401, so do nothing there but register. ### 13. Where plugins live @@ -1497,6 +1554,7 @@ Built per plugin in `src/backend/plugins/ctx.ts` and passed to `activate`. | `ctx.services.provide` / `.get` / `.providers` | per-service RBAC | **A1**, named **B12** | | `ctx.secrets.offer` / `.withdraw` / `.getShared` | per-secret RBAC | **A1** | | `ctx.secrets.get` / `.set` / `.delete` | `secrets:own` | **B18** | +| `ctx.secrets.seal` / `.unseal` | `secrets:own` | **C1** | | `ctx.disposables.add` | none | **A1** | | `ctx.asUser(userId, fn)` | none, always audited | **A1** | | `ctx.currentActor()` | none | **A1** | @@ -1756,6 +1814,13 @@ notification)` delivers to the ones among them that are theirs and `ctx.db.persist`, because a plugin reads its keys on every request that uses them. The AI assistant keeps each provider's key under `provider:` instead of in its providers table. +- **C1** added `ctx.secrets.seal(value)` and `.unseal(sealed)`: the same + installation-key encryption, returned to the plugin instead of stored, for + a secret that lives in the plugin's own table and has to be read without + an acting user (a second factor checked during login). Both need + `secrets:own`; neither needs an actor or writes an audit line. `unseal` + answers null for anything `seal` did not write. The totp plugin keeps its + secrets and backup codes this way. - `ctx.fetch` takes a `signal`. It aborts the request and a streamed body; `timeoutMs` still only covers the wait for the headers, so a long model stream is not cut. @@ -2296,7 +2361,7 @@ 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) and homepage (B19), which import +(B17), ai (B18), homepage (B19), totp and webauthn (C1), 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 diff --git a/packages/plugin-sdk/FINISH-LIST.md b/packages/plugin-sdk/FINISH-LIST.md index f0b8391e9..ebfaf6d18 100644 --- a/packages/plugin-sdk/FINISH-LIST.md +++ b/packages/plugin-sdk/FINISH-LIST.md @@ -453,6 +453,18 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite `web-endpoint`'s "answers 503 while the tunnels plugin is not available" test gets 502. Confirmed present on `dev-2.9.0` before this step (checked by stashing B20's changes and re-running). Owner: D0. +- **C1 (totp):** `totp-migration.ts` moves a user's TOTP secret only when + their data key opens at boot. A 2.8 user whose key is still a pre-2.5.1 + password wrap keeps their enrolment row (fail closed) but has no secret in + `p_totp_enrollments` until a boot after their next password login migrates + the key, so that one login fails at the TOTP step. Likewise, enabling the + totp plugin for the first time without a restart does not move secrets + until the next boot. Decide whether either needs handling (a migration run + on plugin activation, or an admin reset message). Owner: D0. +- **C1 (totp/webauthn):** the `users` columns `totp_secret`, `totp_enabled` + 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. ## Manual checks after 2.9.0 @@ -520,3 +532,10 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite plugin disabled; the AI assistant lists homepage items and says unavailable with homepage off; disable homepage in admin (the tab, its dashboard cards and the toggle disappear) then enable it again. +- TOTP and passkeys: on a 2.8 database a TOTP user is still asked for a + code (and a backup code works once); set up TOTP from Settings > Security, + add a second device, regenerate backup codes, disable it; enabling signs + out other sessions; "remember me" skips it next time; disable the totp + 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. diff --git a/packages/plugin-sdk/src/backend.ts b/packages/plugin-sdk/src/backend.ts index b53d97046..9f294d0d5 100644 --- a/packages/plugin-sdk/src/backend.ts +++ b/packages/plugin-sdk/src/backend.ts @@ -232,6 +232,14 @@ export interface PluginSecrets { set: (key: string, value: string | null) => Promise; /** Removes the acting user's secret. Needs secrets:own. Audited. */ delete: (key: string) => Promise; + /** + * Encrypts a value with the installation key, for a plugin that keeps a + * secret in its own table and has to read it without an acting user (a + * second factor checked during login). Needs secrets:own. Not audited. + */ + seal: (value: string) => Promise; + /** Reverses seal(). Null when the value cannot be decrypted. */ + unseal: (sealed: string) => Promise; offer: ( key: string, resolve: (userId: string) => Promise | string | null, diff --git a/packages/plugin-sdk/src/frontend.ts b/packages/plugin-sdk/src/frontend.ts index ce79fd5d9..ca8bb23ec 100644 --- a/packages/plugin-sdk/src/frontend.ts +++ b/packages/plugin-sdk/src/frontend.ts @@ -466,8 +466,22 @@ export interface LoginMethodContribution { titleKey: string; icon?: IconComponent; component: ComponentType; + /** + * "inline" also draws the method under the password form, for a local + * method people use instead of a password (a passkey). Otherwise it only + * shows in the list of other sign-in methods. + */ + placement?: "inline"; + /** Enrolment, shown in Settings > Security (registering a passkey). */ + enrollment?: ComponentType>; } +/** + * How an SSH keyboard-interactive prompt is shown by TOTPDialog: a code field + * (the default), a password, a menu choice, or a push approval. + */ +export type MFAPromptMode = "totp" | "password" | "menu" | "push"; + /** What the second-factor step hands a factor's UI. */ export interface SecondFactorUIProps { factorId: string; diff --git a/packages/plugin-sdk/src/testing.ts b/packages/plugin-sdk/src/testing.ts index 7b8faba37..99dfcae69 100644 --- a/packages/plugin-sdk/src/testing.ts +++ b/packages/plugin-sdk/src/testing.ts @@ -8,7 +8,7 @@ * The vitest config these run under comes from @termix/plugin-sdk/vitest-preset. */ -import { PluginCapabilityError } from "./backend.js"; +import { LoginMethodError, PluginCapabilityError } from "./backend.js"; import type { PluginContext, PluginDisposables, @@ -118,6 +118,11 @@ export interface FakeContextOptions { * reaches the network by accident. */ fetch?: (url: string, init?: PluginFetchInit) => Promise; + /** + * Makes ctx.auth.recordEnrollment refuse the way core's policy does (trusted + * proxy login on, password login off), with this message and a 409. + */ + refuseEnrollment?: string; } export interface FakeAuthRegistrations { @@ -508,6 +513,12 @@ export function createFakeContext( delete: async (key) => { secretStore.delete(`${actor ?? ""}:${key}`); }, + seal: async (value) => + `sealed:${Buffer.from(value, "utf8").toString("base64")}`, + unseal: async (sealed) => + sealed?.startsWith("sealed:") + ? Buffer.from(sealed.slice(7), "base64").toString("utf8") + : null, offer: () => {}, withdraw: () => false, getShared: async () => null, @@ -782,6 +793,9 @@ export function createFakeContext( auth.secondFactors.push(factor); }, recordEnrollment: async (userId, factorId) => { + if (options.refuseEnrollment) { + throw new LoginMethodError(options.refuseEnrollment, 409); + } auth.enrollments.add(`${userId}:${factorId}`); }, removeEnrollment: async (userId, factorId) => { @@ -927,6 +941,8 @@ export interface MockContextOptions { * reaches the network by accident. */ fetch?: (url: string, init?: PluginFetchInit) => Promise; + /** See FakeContextOptions. */ + refuseEnrollment?: string; } export interface MockPluginContext extends FakePluginContext { @@ -969,6 +985,7 @@ export function createMockCtx( hostStatuses: options.hostStatuses, notificationChannels: options.notificationChannels, fetch: options.fetch, + refuseEnrollment: options.refuseEnrollment, manifest: { capabilities: options.capabilities ?? [], ...options.manifest, @@ -1271,6 +1288,14 @@ export function createMockCtx( require("secrets:own"); return ctx.secrets.delete(key); }, + seal: async (value) => { + require("secrets:own"); + return ctx.secrets.seal(value); + }, + unseal: async (sealed) => { + require("secrets:own"); + return ctx.secrets.unseal(sealed); + }, }, capabilities: { @@ -1485,6 +1510,8 @@ export interface RenderedPluginApp { settingsComponents: () => string[]; slot: (slotId: string) => string[]; actions: () => string[]; + loginMethods: () => string[]; + secondFactors: () => string[]; }; /** Shell calls made by the plugin's code, applyLayout included. */ shellCalls: ShellCall[]; @@ -1510,6 +1537,18 @@ export interface RenderedPluginApp { componentId: string, props?: Record, ) => HTMLElement; + /** Renders a login method's UI with login screen props, overridable. */ + renderLoginMethod: ( + id: string, + props?: Record, + ) => HTMLElement; + /** Renders a second factor's challenge with overridable props. */ + renderSecondFactor: ( + id: string, + props?: Record, + ) => HTMLElement; + /** Renders the Settings > Security section a factor or method brought. */ + renderEnrollment: (id: string) => HTMLElement; /** Renders a slot the way its owner would. */ renderSlot: (slotId: string, props?: Record) => HTMLElement; /** Runs deactivate and every disposer, as disabling the plugin does. */ diff --git a/plugins/totp/locales/en.json b/plugins/totp/locales/en.json new file mode 100644 index 000000000..4610c7ac3 --- /dev/null +++ b/plugins/totp/locales/en.json @@ -0,0 +1,42 @@ +{ + "title": "Authenticator App", + "factor": "Authenticator code", + "challenge": { + "enterCode": "Enter verification code", + "verify": "Verify Code", + "backupCodeHint": "You can also enter one of your backup codes." + }, + "enrollment": { + "title": "TOTP Authenticator", + "docs": "Docs", + "on": "2FA is enabled", + "off": "Add extra login security", + "enable": "Enable", + "disable": "Disable", + "addDevice": "Add device", + "addTitle": "Add authenticator device", + "addDescription": "Confirm with a code from your authenticator or a backup code to enroll another device with the same key.", + "addScanInstructions": "Scan this QR code with the additional authenticator. Existing authenticators will continue to work.", + "addSuccess": "Additional authenticator enrolled", + "addFailed": "Failed to add authenticator", + "codeRequired": "Enter a TOTP code or backup code", + "codePlaceholder": "TOTP code or backup code", + "setupTitle": "Setup TOTP", + "setupFailed": "Failed to start TOTP setup", + "qrCode": "QR Code", + "secretCopied": "Secret copied to clipboard", + "instructions": "Scan the QR code or enter the secret in your authenticator app, then enter the 6-digit code", + "continueVerify": "Continue to Verify", + "verifyTitle": "Verify Code", + "verify": "Verify", + "enter6Digits": "Enter a 6-digit code", + "enabledSuccess": "Two-factor authentication enabled", + "invalidCode": "Invalid code, please try again", + "disableTitle": "Disable 2FA", + "disabledSuccess": "Two-factor authentication disabled", + "disableFailed": "Failed to disable 2FA", + "backupTitle": "Backup Codes", + "downloadBackup": "Download Backup Codes", + "done": "Done" + } +} diff --git a/plugins/totp/locales/translated/af_ZA.json b/plugins/totp/locales/translated/af_ZA.json new file mode 100644 index 000000000..ca7c8c3fd --- /dev/null +++ b/plugins/totp/locales/translated/af_ZA.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "Voer verifikasiekode in", + "verify": "Verifieer Kode" + }, + "enrollment": { + "title": "TOTP-verifikasie", + "on": "2FA is geaktiveer", + "off": "Voeg ekstra aanmeldsekuriteit by", + "enable": "Aktiveer", + "disable": "Deaktiveer", + "addDevice": "Voeg toestel by", + "addTitle": "Voeg verifikasietoestel by", + "addScanInstructions": "Skandeer hierdie QR-kode met die bykomende verifikator. Bestaande verifikators sal aanhou werk.", + "addSuccess": "Bykomende outentiseerder ingeskryf", + "addFailed": "Kon nie verifieerder byvoeg nie", + "setupTitle": "Stel TOTP op", + "setupFailed": "Kon nie TOTP-opstelling begin nie", + "qrCode": "QR-kode", + "secretCopied": "Geheim gekopieer na knipbord", + "continueVerify": "Gaan voort om te verifieer", + "verifyTitle": "Verifieer Kode", + "verify": "Verifieer", + "enter6Digits": "Voer 'n 6-syferkode in", + "enabledSuccess": "Tweefaktor-verifikasie geaktiveer", + "invalidCode": "Ongeldige kode, probeer asseblief weer", + "disableTitle": "Deaktiveer 2FA", + "disabledSuccess": "Tweefaktor-verifikasie gedeaktiveer", + "disableFailed": "Kon nie 2FA deaktiveer nie", + "backupTitle": "Rugsteunkodes", + "downloadBackup": "Laai rugsteunkodes af", + "done": "Klaar" + } +} diff --git a/plugins/totp/locales/translated/ar_SA.json b/plugins/totp/locales/translated/ar_SA.json new file mode 100644 index 000000000..8c2c818ae --- /dev/null +++ b/plugins/totp/locales/translated/ar_SA.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "أدخل رمز التحقق", + "verify": "التحقق من الرمز" + }, + "enrollment": { + "title": "مُوثِّق TOTP", + "on": "التحقق بخطوتين ممكّن", + "off": "إضافة أمان إضافي لتسجيل الدخول", + "enable": "تمكين", + "disable": "تعطيل", + "addDevice": "إضافة جهاز", + "addTitle": "إضافة جهاز المصادقة", + "addScanInstructions": "امسح رمز الاستجابة السريعة هذا باستخدام أداة المصادقة الإضافية. ستستمر أدوات المصادقة الحالية في العمل.", + "addSuccess": "تم تسجيل مصادق إضافي", + "addFailed": "فشل إضافة المصادقة", + "setupTitle": "إعداد TOTP", + "setupFailed": "فشل بدء إعداد TOTP", + "qrCode": "رمز الاستجابة السريعة", + "secretCopied": "تم نسخ السر إلى الحافظة", + "continueVerify": "متابعة للتحقق", + "verifyTitle": "التحقق من الرمز", + "verify": "تحقق", + "enter6Digits": "أدخل رمزًا من 6 أرقام", + "enabledSuccess": "تم تمكين المصادقة الثنائية", + "invalidCode": "رمز غير صالح، يرجى المحاولة مرة أخرى", + "disableTitle": "تعطيل المصادقة الثنائية", + "disabledSuccess": "تم تعطيل المصادقة الثنائية", + "disableFailed": "فشل تعطيل المصادقة الثنائية", + "backupTitle": "رموز النسخ الاحتياطي", + "downloadBackup": "تنزيل رموز النسخ الاحتياطي", + "done": "تم" + } +} diff --git a/plugins/totp/locales/translated/bg_BG.json b/plugins/totp/locales/translated/bg_BG.json new file mode 100644 index 000000000..c59370f12 --- /dev/null +++ b/plugins/totp/locales/translated/bg_BG.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "Въведете код за потвърждение", + "verify": "Код за проверка" + }, + "enrollment": { + "title": "TOTP удостоверител", + "on": "2FA е активирана", + "off": "Добавете допълнителна сигурност за вход", + "enable": "Активиране", + "disable": "Деактивиране", + "addDevice": "Добавяне на устройство", + "addTitle": "Добавяне на устройство за удостоверяване", + "addScanInstructions": "Сканирайте този QR код с допълнителния автентификатор. Съществуващите автентификатори ще продължат да работят.", + "addSuccess": "Допълнителен регистриран удостоверител", + "addFailed": "Добавянето на удостоверител не бе успешно", + "setupTitle": "Настройка на TOTP", + "setupFailed": "Неуспешно стартиране на настройката на TOTP", + "qrCode": "QR код", + "secretCopied": "Тайната е копирана в клипборда", + "continueVerify": "Продължете с проверката", + "verifyTitle": "Код за проверка", + "verify": "Проверка", + "enter6Digits": "Въведете 6-цифрен код", + "enabledSuccess": "Двуфакторно удостоверяване е активирано", + "invalidCode": "Невалиден код, моля опитайте отново", + "disableTitle": "Деактивиране на 2FA", + "disabledSuccess": "Двуфакторното удостоверяване е деактивирано", + "disableFailed": "Деактивирането на 2FA не бе успешно", + "backupTitle": "Резервни кодове", + "downloadBackup": "Изтегляне на резервни кодове", + "done": "Готово" + } +} diff --git a/plugins/totp/locales/translated/bn_BD.json b/plugins/totp/locales/translated/bn_BD.json new file mode 100644 index 000000000..09c2a2be5 --- /dev/null +++ b/plugins/totp/locales/translated/bn_BD.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "যাচাইকরণ কোড প্রবেশ করান", + "verify": "কোড যাচাই করুন" + }, + "enrollment": { + "title": "TOTP প্রমাণীকরণকারী", + "on": "2FA সক্রিয় করা হয়েছে", + "off": "অতিরিক্ত লগইন নিরাপত্তা যোগ করুন", + "enable": "সক্ষম করুন", + "disable": "অক্ষম করুন", + "addDevice": "ডিভাইস যোগ করুন", + "addTitle": "প্রমাণীকরণ ডিভাইস যোগ করুন", + "addScanInstructions": "অতিরিক্ত অথেন্টিকেটর দিয়ে এই কিউআর কোডটি স্ক্যান করুন। বিদ্যমান অথেন্টিকেটরগুলো কাজ করতে থাকবে।", + "addSuccess": "অতিরিক্ত প্রমাণীকরণকারী নথিভুক্ত করা হয়েছে", + "addFailed": "প্রমাণীকরণকারী যোগ করতে ব্যর্থ হয়েছে", + "setupTitle": "TOTP সেটআপ করুন", + "setupFailed": "TOTP সেটআপ শুরু করতে ব্যর্থ হয়েছে", + "qrCode": "কিউআর কোড", + "secretCopied": "গোপনীয় তথ্য ক্লিপবোর্ডে কপি করা হয়েছে", + "continueVerify": "যাচাই চালিয়ে যান", + "verifyTitle": "কোড যাচাই করুন", + "verify": "যাচাই করুন", + "enter6Digits": "একটি ৬-সংখ্যার কোড প্রবেশ করান", + "enabledSuccess": "দ্বি-স্তর প্রমাণীকরণ সক্রিয় করা হয়েছে", + "invalidCode": "কোডটি ভুল, অনুগ্রহ করে আবার চেষ্টা করুন।", + "disableTitle": "2FA নিষ্ক্রিয় করুন", + "disabledSuccess": "দ্বি-স্তর প্রমাণীকরণ নিষ্ক্রিয় করা হয়েছে", + "disableFailed": "2FA নিষ্ক্রিয় করতে ব্যর্থ হয়েছে", + "backupTitle": "ব্যাকআপ কোড", + "downloadBackup": "ব্যাকআপ কোড ডাউনলোড করুন", + "done": "সম্পন্ন হয়েছে" + } +} diff --git a/plugins/totp/locales/translated/ca_ES.json b/plugins/totp/locales/translated/ca_ES.json new file mode 100644 index 000000000..40ab08cff --- /dev/null +++ b/plugins/totp/locales/translated/ca_ES.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "Introduïu el codi de verificació", + "verify": "Verifica el codi" + }, + "enrollment": { + "title": "Autenticador TOTP", + "on": "2FA està activat", + "off": "Afegeix seguretat d'inici de sessió addicional", + "enable": "Activa", + "disable": "Desactiva", + "addDevice": "Afegeix un dispositiu", + "addTitle": "Afegeix un dispositiu d'autenticació", + "addScanInstructions": "Escaneja aquest codi QR amb l'autenticador addicional. Els autenticadors existents continuaran funcionant.", + "addSuccess": "Autenticador addicional inscrit", + "addFailed": "No s'ha pogut afegir l'autenticador", + "setupTitle": "Configura TOTP", + "setupFailed": "No s'ha pogut iniciar la configuració del TOTP", + "qrCode": "Codi QR", + "secretCopied": "Secret copiat al porta-retalls", + "continueVerify": "Continua a verificar", + "verifyTitle": "Verifica el codi", + "verify": "Verifica", + "enter6Digits": "Introduïu un codi de 6 dígits", + "enabledSuccess": "Autenticació de dos factors activada", + "invalidCode": "Codi no vàlid, si us plau, torna-ho a intentar.", + "disableTitle": "Desactiva la 2FA", + "disabledSuccess": "Autenticació de dos factors desactivada", + "disableFailed": "No s'ha pogut desactivar la 2FA", + "backupTitle": "Codis de còpia de seguretat", + "downloadBackup": "Baixa els codis de còpia de seguretat", + "done": "Fet" + } +} diff --git a/plugins/totp/locales/translated/cs_CZ.json b/plugins/totp/locales/translated/cs_CZ.json new file mode 100644 index 000000000..6a7bdfd74 --- /dev/null +++ b/plugins/totp/locales/translated/cs_CZ.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "Zadejte ověřovací kód", + "verify": "Ověřte kód" + }, + "enrollment": { + "title": "Ověřovač TOTP", + "on": "2FA je povoleno", + "off": "Přidejte další zabezpečení přihlášení", + "enable": "Umožnit", + "disable": "Zakázat", + "addDevice": "Přidat zařízení", + "addTitle": "Přidat ověřovací zařízení", + "addScanInstructions": "Naskenujte tento QR kód pomocí dalšího ověřovače. Stávající ověřovače budou i nadále fungovat.", + "addSuccess": "Další ověřovatel zaregistrován", + "addFailed": "Přidání ověřovatele se nezdařilo", + "setupTitle": "Nastavení TOTP", + "setupFailed": "Nepodařilo se spustit nastavení TOTP", + "qrCode": "QR kód", + "secretCopied": "Tajný kód zkopírován do schránky", + "continueVerify": "Pokračovat k ověřování", + "verifyTitle": "Ověřte kód", + "verify": "Ověřit", + "enter6Digits": "Zadejte 6místný kód", + "enabledSuccess": "Dvoufázové ověřování povoleno", + "invalidCode": "Neplatný kód, zkuste to prosím znovu.", + "disableTitle": "Zakázat 2FA", + "disabledSuccess": "Dvoufázové ověřování zakázáno", + "disableFailed": "Nepodařilo se zakázat 2FA", + "backupTitle": "Záložní kódy", + "downloadBackup": "Stáhnout záložní kódy", + "done": "Hotovo" + } +} diff --git a/plugins/totp/locales/translated/da_DK.json b/plugins/totp/locales/translated/da_DK.json new file mode 100644 index 000000000..47aa7e3b7 --- /dev/null +++ b/plugins/totp/locales/translated/da_DK.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "Indtast bekræftelseskode", + "verify": "Bekræft kode" + }, + "enrollment": { + "title": "TOTP-godkendelse", + "on": "2FA er aktiveret", + "off": "Tilføj ekstra loginsikkerhed", + "enable": "Aktivér", + "disable": "Deaktiver", + "addDevice": "Tilføj enhed", + "addTitle": "Tilføj godkendelsesenhed", + "addScanInstructions": "Scan denne QR-kode med den ekstra godkendelsesfunktion. Eksisterende godkendelsesfunktioner vil fortsat virke.", + "addSuccess": "Yderligere godkendelsesfunktion tilmeldt", + "addFailed": "Kunne ikke tilføje godkendelse", + "setupTitle": "Opsæt TOTP", + "setupFailed": "Kunne ikke starte TOTP-opsætningen", + "qrCode": "QR-kode", + "secretCopied": "Hemmelighed kopieret til udklipsholder", + "continueVerify": "Fortsæt med at bekræfte", + "verifyTitle": "Bekræft kode", + "verify": "Verificere", + "enter6Digits": "Indtast en 6-cifret kode", + "enabledSuccess": "Tofaktorgodkendelse aktiveret", + "invalidCode": "Ugyldig kode, prøv igen", + "disableTitle": "Deaktiver 2FA", + "disabledSuccess": "Tofaktorgodkendelse deaktiveret", + "disableFailed": "Kunne ikke deaktivere 2FA", + "backupTitle": "Backupkoder", + "downloadBackup": "Download backupkoder", + "done": "Færdig" + } +} diff --git a/plugins/totp/locales/translated/de_DE.json b/plugins/totp/locales/translated/de_DE.json new file mode 100644 index 000000000..d2ce861a4 --- /dev/null +++ b/plugins/totp/locales/translated/de_DE.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "Bestätigungscode eingeben", + "verify": "Code überprüfen" + }, + "enrollment": { + "title": "TOTP-Authentifikator", + "on": "2FA ist aktiviert", + "off": "Zusätzliche Anmeldesicherheit hinzufügen", + "enable": "Aktivieren", + "disable": "Deaktivieren", + "addDevice": "Gerät hinzufügen", + "addTitle": "Authentifizierungsgerät hinzufügen", + "addScanInstructions": "Scannen Sie diesen QR-Code mit dem zusätzlichen Authentifikator. Bestehende Authentifikatoren funktionieren weiterhin.", + "addSuccess": "Zusätzlicher Authentifizierungsalgorithmus registriert", + "addFailed": "Authentifikator konnte nicht hinzugefügt werden", + "setupTitle": "TOTP einrichten", + "setupFailed": "Fehler beim Starten der TOTP-Einrichtung", + "qrCode": "QR-Code", + "secretCopied": "Secret in die Zwischenablage kopiert", + "continueVerify": "Weiter zur Verifizierung", + "verifyTitle": "Code verifizieren", + "verify": "Verifizieren", + "enter6Digits": "Geben Sie einen 6-stelligen Code ein", + "enabledSuccess": "Zwei-Faktor-Authentifizierung aktiviert", + "invalidCode": "Ungültiger Code, bitte versuchen Sie es erneut", + "disableTitle": "2FA deaktivieren", + "disabledSuccess": "Zwei-Faktor-Authentifizierung deaktiviert", + "disableFailed": "Fehler beim Deaktivieren der 2FA", + "backupTitle": "Backup-Codes", + "downloadBackup": "Backup-Codes herunterladen", + "done": "Fertig" + } +} diff --git a/plugins/totp/locales/translated/el_GR.json b/plugins/totp/locales/translated/el_GR.json new file mode 100644 index 000000000..a3034258f --- /dev/null +++ b/plugins/totp/locales/translated/el_GR.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "Εισαγάγετε τον κωδικό επαλήθευσης", + "verify": "Επαλήθευση Κωδικού" + }, + "enrollment": { + "title": "Επαληθευτής TOTP", + "on": "Το 2FA είναι ενεργοποιημένο", + "off": "Προσθέστε επιπλέον ασφάλεια σύνδεσης", + "enable": "Καθιστώ ικανό", + "disable": "Καθιστώ ανίκανο", + "addDevice": "Προσθήκη συσκευής", + "addTitle": "Προσθήκη συσκευής ελέγχου ταυτότητας", + "addScanInstructions": "Σαρώστε αυτόν τον κωδικό QR με τον πρόσθετο έλεγχο ταυτότητας. Οι υπάρχοντες έλεγχοι ταυτότητας θα συνεχίσουν να λειτουργούν.", + "addSuccess": "Εγγράφηκε επιπλέον υπεύθυνος ελέγχου ταυτότητας", + "addFailed": "Αποτυχία προσθήκης ελέγχου ταυτότητας", + "setupTitle": "Ρύθμιση TOTP", + "setupFailed": "Αποτυχία έναρξης ρύθμισης TOTP", + "qrCode": "Κωδικός QR", + "secretCopied": "Το μυστικό αντιγράφηκε στο πρόχειρο", + "continueVerify": "Συνέχεια για επαλήθευση", + "verifyTitle": "Επαλήθευση Κωδικού", + "verify": "Επαληθεύω", + "enter6Digits": "Εισαγάγετε έναν 6ψήφιο κωδικό", + "enabledSuccess": "Ενεργοποιήθηκε ο έλεγχος ταυτότητας δύο παραγόντων", + "invalidCode": "Μη έγκυρος κωδικός, δοκιμάστε ξανά", + "disableTitle": "Απενεργοποίηση 2FA", + "disabledSuccess": "Ο έλεγχος ταυτότητας δύο παραγόντων απενεργοποιήθηκε", + "disableFailed": "Αποτυχία απενεργοποίησης 2FA", + "backupTitle": "Κωδικοί αντιγράφων ασφαλείας", + "downloadBackup": "Λήψη αντιγράφων ασφαλείας κωδικών", + "done": "Γινώμενος" + } +} diff --git a/plugins/totp/locales/translated/es_ES.json b/plugins/totp/locales/translated/es_ES.json new file mode 100644 index 000000000..e9930c89e --- /dev/null +++ b/plugins/totp/locales/translated/es_ES.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "Introduzca el código de verificación", + "verify": "Verificar código" + }, + "enrollment": { + "title": "Autenticador TOTP", + "on": "2FA activada", + "off": "Añadir seguridad adicional al inicio de sesión", + "enable": "Activar", + "disable": "Desactivar", + "addDevice": "Agregar dispositivo", + "addTitle": "Agregar dispositivo de autenticación", + "addScanInstructions": "Escanee este código QR con el autenticador adicional. Los autenticadores existentes seguirán funcionando.", + "addSuccess": "Autenticador adicional registrado", + "addFailed": "No se pudo agregar el autenticador.", + "setupTitle": "Configurar TOTP", + "setupFailed": "No se pudo iniciar la configuración de TOTP", + "qrCode": "Código QR", + "secretCopied": "Secreto copiado al portapapeles", + "continueVerify": "Continuar para verificar", + "verifyTitle": "Verificar código", + "verify": "Verificar", + "enter6Digits": "Introduce un código de 6 dígitos", + "enabledSuccess": "Autenticación de dos factores activada", + "invalidCode": "Código no válido, inténtalo de nuevo", + "disableTitle": "Desactivar 2FA", + "disabledSuccess": "Autenticación de dos factores desactivada", + "disableFailed": "No se pudo desactivar 2FA", + "backupTitle": "Códigos de respaldo", + "downloadBackup": "Descargar códigos de respaldo", + "done": "Listo" + } +} diff --git a/plugins/totp/locales/translated/fi_FI.json b/plugins/totp/locales/translated/fi_FI.json new file mode 100644 index 000000000..e157538fd --- /dev/null +++ b/plugins/totp/locales/translated/fi_FI.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "Syötä vahvistuskoodi", + "verify": "Vahvista koodi" + }, + "enrollment": { + "title": "TOTP-todentaja", + "on": "2FA on käytössä", + "off": "Lisää kirjautumisen turvallisuutta", + "enable": "Ota käyttöön", + "disable": "Poista käytöstä", + "addDevice": "Lisää laite", + "addTitle": "Lisää todennuslaite", + "addScanInstructions": "Skannaa tämä QR-koodi lisätodentajalla. Nykyiset todentajat toimivat edelleen.", + "addSuccess": "Lisätodentaja rekisteröity", + "addFailed": "Todentajan lisääminen epäonnistui", + "setupTitle": "TOTP:n määrittäminen", + "setupFailed": "TOTP-asennuksen aloittaminen epäonnistui", + "qrCode": "QR-koodi", + "secretCopied": "Salaisuus kopioitu leikepöydälle", + "continueVerify": "Jatka vahvistamiseen", + "verifyTitle": "Vahvista koodi", + "verify": "Vahvista", + "enter6Digits": "Syötä 6-numeroinen koodi", + "enabledSuccess": "Kaksivaiheinen todennus käytössä", + "invalidCode": "Virheellinen koodi, yritä uudelleen", + "disableTitle": "Poista 2FA käytöstä", + "disabledSuccess": "Kaksivaiheinen todennus poistettu käytöstä", + "disableFailed": "2FA:n poistaminen käytöstä epäonnistui", + "backupTitle": "Varakoodit", + "downloadBackup": "Lataa varakoodit", + "done": "Tehty" + } +} diff --git a/plugins/totp/locales/translated/fr_FR.json b/plugins/totp/locales/translated/fr_FR.json new file mode 100644 index 000000000..627818bb2 --- /dev/null +++ b/plugins/totp/locales/translated/fr_FR.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "Saisir le code", + "verify": "Vérifier le code" + }, + "enrollment": { + "title": "Authentificateur TOTP", + "on": "2FA activée", + "off": "Ajouter une sécurité de connexion supplémentaire", + "enable": "Activer", + "disable": "Désactiver", + "addDevice": "Ajouter un appareil", + "addTitle": "Ajouter un dispositif d'authentification", + "addScanInstructions": "Scannez ce code QR avec l'authentificateur supplémentaire. Les authentificateurs existants continueront de fonctionner.", + "addSuccess": "Authentificateur supplémentaire enregistré", + "addFailed": "Échec de l'ajout de l'authentificateur", + "setupTitle": "Configurer TOTP", + "setupFailed": "Échec du démarrage de la configuration TOTP", + "qrCode": "Code QR", + "secretCopied": "Secret copié dans le presse-papiers", + "continueVerify": "Poursuivre la vérification", + "verifyTitle": "Vérifier le code", + "verify": "Vérifier", + "enter6Digits": "Saisissez un code à 6 chiffres", + "enabledSuccess": "Authentification à deux facteurs activée", + "invalidCode": "Code invalide, veuillez réessayer", + "disableTitle": "Désactiver l’A2F", + "disabledSuccess": "Authentification à deux facteurs désactivée", + "disableFailed": "Échec de la désactivation de la 2FA", + "backupTitle": "Codes de sauvegarde", + "downloadBackup": "Télécharger les codes de sauvegarde", + "done": "Terminé" + } +} diff --git a/plugins/totp/locales/translated/he_IL.json b/plugins/totp/locales/translated/he_IL.json new file mode 100644 index 000000000..1d0269578 --- /dev/null +++ b/plugins/totp/locales/translated/he_IL.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "הזן קוד אימות", + "verify": "אימות קוד" + }, + "enrollment": { + "title": "מאמת TOTP", + "on": "2FA מופעל", + "off": "הוסף אבטחת התחברות נוספת", + "enable": "לְאַפשֵׁר", + "disable": "השבת", + "addDevice": "הוסף מכשיר", + "addTitle": "הוסף מכשיר אימות", + "addScanInstructions": "סרוק את קוד ה-QR הזה עם המאמת הנוסף. המאמתים הקיימים ימשיכו לפעול.", + "addSuccess": "מאמת נוסף נרשם", + "addFailed": "נכשלה הוספת המאמת", + "setupTitle": "הגדר TOTP", + "setupFailed": "נכשל בהתחלת הגדרת TOTP", + "qrCode": "קוד QR", + "secretCopied": "סוד הועתק ללוח", + "continueVerify": "המשך לאימות", + "verifyTitle": "אימות קוד", + "verify": "לְאַמֵת", + "enter6Digits": "הזן קוד בן 6 ספרות", + "enabledSuccess": "אימות דו-שלבי מופעל", + "invalidCode": "קוד לא תקין, אנא נסה שוב", + "disableTitle": "השבת 2FA", + "disabledSuccess": "אימות דו-שלבי מושבת", + "disableFailed": "נכשל בהשבתת 2FA", + "backupTitle": "קודי גיבוי", + "downloadBackup": "הורד קודי גיבוי", + "done": "נַעֲשָׂה" + } +} diff --git a/plugins/totp/locales/translated/hi_IN.json b/plugins/totp/locales/translated/hi_IN.json new file mode 100644 index 000000000..1c887116d --- /dev/null +++ b/plugins/totp/locales/translated/hi_IN.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "सत्यापन कोड दर्ज करें", + "verify": "कोड सत्यापित करें" + }, + "enrollment": { + "title": "TOTP प्रमाणक", + "on": "2FA सक्षम है", + "off": "अतिरिक्त लॉगिन सुरक्षा जोड़ें", + "enable": "सक्षम करें", + "disable": "अक्षम करें", + "addDevice": "डिवाइस जोडे", + "addTitle": "प्रमाणीकरण उपकरण जोड़ें", + "addScanInstructions": "इस क्यूआर कोड को अतिरिक्त प्रमाणीकरण उपकरण से स्कैन करें। मौजूदा प्रमाणीकरण उपकरण काम करते रहेंगे।", + "addSuccess": "अतिरिक्त प्रमाणीकरणकर्ता नामांकित किया गया", + "addFailed": "प्रमाणीकरणकर्ता जोड़ने में विफल", + "setupTitle": "TOTP सेटअप करें", + "setupFailed": "TOTP सेटअप शुरू करने में विफल", + "qrCode": "QR कोड", + "secretCopied": "सीक्रेट क्लिपबोर्ड पर कॉपी किया गया", + "continueVerify": "सत्यापन जारी रखें", + "verifyTitle": "कोड सत्यापित करें", + "verify": "सत्यापित करें", + "enter6Digits": "6-अंकीय कोड दर्ज करें", + "enabledSuccess": "दो-कारक प्रमाणीकरण सक्षम किया गया", + "invalidCode": "अमान्य कोड, कृपया पुनः प्रयास करें", + "disableTitle": "2FA अक्षम करें", + "disabledSuccess": "दो-कारक प्रमाणीकरण अक्षम किया गया", + "disableFailed": "2FA अक्षम करने में विफल", + "backupTitle": "बैकअप कोड", + "downloadBackup": "बैकअप कोड डाउनलोड करें", + "done": "पूर्ण" + } +} diff --git a/plugins/totp/locales/translated/hu_HU.json b/plugins/totp/locales/translated/hu_HU.json new file mode 100644 index 000000000..2f2685102 --- /dev/null +++ b/plugins/totp/locales/translated/hu_HU.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "Írja be az ellenőrző kódot", + "verify": "Kód ellenőrzése" + }, + "enrollment": { + "title": "TOTP hitelesítő", + "on": "2FA engedélyezve", + "off": "Fokozott bejelentkezési biztonság", + "enable": "Engedélyezés", + "disable": "Letiltás", + "addDevice": "Eszköz hozzáadása", + "addTitle": "Hitelesítő eszköz hozzáadása", + "addScanInstructions": "Olvasd be ezt a QR-kódot a további hitelesítővel. A meglévő hitelesítők továbbra is működni fognak.", + "addSuccess": "További hitelesítő regisztrálva", + "addFailed": "Nem sikerült hozzáadni a hitelesítőt", + "setupTitle": "TOTP beállítása", + "setupFailed": "Nem sikerült elindítani a TOTP beállítását", + "qrCode": "QR-kód", + "secretCopied": "Titkos kód a vágólapra másolva", + "continueVerify": "Tovább az ellenőrzéshez", + "verifyTitle": "Kód ellenőrzése", + "verify": "Ellenőrzés", + "enter6Digits": "Adjon meg egy 6 jegyű kódot", + "enabledSuccess": "Kétfaktoros hitelesítés engedélyezve", + "invalidCode": "Érvénytelen kód, kérjük, próbálja újra", + "disableTitle": "2FA letiltása", + "disabledSuccess": "Kétfaktoros hitelesítés letiltva", + "disableFailed": "Nem sikerült letiltani a 2FA-t", + "backupTitle": "Biztonsági kódok", + "downloadBackup": "Tartalékkódok letöltése", + "done": "Kész" + } +} diff --git a/plugins/totp/locales/translated/id_ID.json b/plugins/totp/locales/translated/id_ID.json new file mode 100644 index 000000000..47b2fe31c --- /dev/null +++ b/plugins/totp/locales/translated/id_ID.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "Masukkan kode verifikasi", + "verify": "Verifikasi Kode" + }, + "enrollment": { + "title": "Otentikator TOTP", + "on": "Otentikasi dua faktor (2FA) diaktifkan.", + "off": "Tambahkan keamanan login ekstra", + "enable": "Memungkinkan", + "disable": "Cacat", + "addDevice": "Tambahkan perangkat", + "addTitle": "Tambahkan perangkat otentikasi", + "addScanInstructions": "Pindai kode QR ini dengan otentikator tambahan. Otentikator yang sudah ada akan tetap berfungsi.", + "addSuccess": "Autentikator tambahan terdaftar", + "addFailed": "Gagal menambahkan autentikator", + "setupTitle": "Pengaturan TOTP", + "setupFailed": "Gagal memulai pengaturan TOTP", + "qrCode": "Kode QR", + "secretCopied": "Rahasia disalin ke papan klip", + "continueVerify": "Lanjutkan Verifikasi", + "verifyTitle": "Verifikasi Kode", + "verify": "Memeriksa", + "enter6Digits": "Masukkan kode 6 digit", + "enabledSuccess": "Autentikasi dua faktor diaktifkan", + "invalidCode": "Kode tidak valid, silakan coba lagi.", + "disableTitle": "Nonaktifkan otentikasi dua faktor (2FA).", + "disabledSuccess": "Autentikasi dua faktor dinonaktifkan", + "disableFailed": "Gagal menonaktifkan 2FA.", + "backupTitle": "Kode Cadangan", + "downloadBackup": "Unduh Kode Cadangan", + "done": "Selesai" + } +} diff --git a/plugins/totp/locales/translated/it_IT.json b/plugins/totp/locales/translated/it_IT.json new file mode 100644 index 000000000..416d19e63 --- /dev/null +++ b/plugins/totp/locales/translated/it_IT.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "Inserisci codice di verifica", + "verify": "Verifica codice" + }, + "enrollment": { + "title": "Autenticatore TOTP", + "on": "2FA è attivata", + "off": "Aggiungi sicurezza aggiuntiva all'accesso", + "enable": "Abilita", + "disable": "Disabilita", + "addDevice": "Aggiungi dispositivo", + "addTitle": "Aggiungi dispositivo di autenticazione", + "addScanInstructions": "Scansiona questo codice QR con l'autenticatore aggiuntivo. Gli autenticatori già in uso continueranno a funzionare.", + "addSuccess": "Autenticatore aggiuntivo registrato", + "addFailed": "Impossibile aggiungere l'autenticatore", + "setupTitle": "Configura TOTP", + "setupFailed": "Impossibile avviare la configurazione TOTP", + "qrCode": "Codice QR", + "secretCopied": "Segreto copiato negli appunti", + "continueVerify": "Procedi alla verifica", + "verifyTitle": "Verifica codice", + "verify": "Verifica", + "enter6Digits": "Inserisci un codice a 6 cifre", + "enabledSuccess": "Autenticazione a due fattori abilitata", + "invalidCode": "Codice non valido, riprova", + "disableTitle": "Disabilita 2FA", + "disabledSuccess": "Autenticazione a due fattori disabilitata", + "disableFailed": "Impossibile disabilitare la 2FA", + "backupTitle": "Codici di backup", + "downloadBackup": "Scarica i codici di backup", + "done": "Fatto" + } +} diff --git a/plugins/totp/locales/translated/ja_JP.json b/plugins/totp/locales/translated/ja_JP.json new file mode 100644 index 000000000..428ef0d53 --- /dev/null +++ b/plugins/totp/locales/translated/ja_JP.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "認証コードを入力", + "verify": "コードを確認" + }, + "enrollment": { + "title": "TOTP認証", + "on": "2FAが有効です", + "off": "ログインのセキュリティを強化", + "enable": "有効にする", + "disable": "無効にする", + "addDevice": "デバイスを追加する", + "addTitle": "認証デバイスを追加する", + "addScanInstructions": "追加の認証デバイスでこのQRコードをスキャンしてください。既存の認証デバイスは引き続きご利用いただけます。", + "addSuccess": "追加の認証デバイスを登録しました", + "addFailed": "認証システムの追加に失敗しました", + "setupTitle": "TOTPを設定", + "setupFailed": "TOTPのセットアップを開始できませんでした", + "qrCode": "QRコード", + "secretCopied": "シークレットをクリップボードにコピーしました", + "continueVerify": "確認に進む", + "verifyTitle": "コードの確認", + "verify": "確認", + "enter6Digits": "6桁のコードを入力してください", + "enabledSuccess": "二要素認証が有効になりました", + "invalidCode": "コードが無効です。もう一度お試しください", + "disableTitle": "2FAを無効にする", + "disabledSuccess": "二要素認証が無効になりました", + "disableFailed": "2FAの無効化に失敗しました", + "backupTitle": "バックアップコード", + "downloadBackup": "バックアップコードをダウンロード", + "done": "完了" + } +} diff --git a/plugins/totp/locales/translated/ko_KR.json b/plugins/totp/locales/translated/ko_KR.json new file mode 100644 index 000000000..a962924d8 --- /dev/null +++ b/plugins/totp/locales/translated/ko_KR.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "인증 코드 입력", + "verify": "코드 확인" + }, + "enrollment": { + "title": "TOTP 인증기", + "on": "2FA가 활성화되었습니다", + "off": "추가 로그인 보안을 설정하세요", + "enable": "활성화", + "disable": "비활성화", + "addDevice": "기기 추가", + "addTitle": "인증 장치 추가", + "addScanInstructions": "추가 인증 수단으로 이 QR 코드를 스캔하세요. 기존 인증 수단도 계속 사용할 수 있습니다.", + "addSuccess": "추가 인증자가 등록되었습니다", + "addFailed": "인증자 추가에 실패했습니다.", + "setupTitle": "TOTP 설정", + "setupFailed": "TOTP 설정 시작 실패", + "qrCode": "QR 코드", + "secretCopied": "시크릿이 클립보드에 복사됨", + "continueVerify": "인증 계속하기", + "verifyTitle": "코드 확인", + "verify": "확인", + "enter6Digits": "6자리 코드 입력", + "enabledSuccess": "2단계 인증이 활성화되었습니다.", + "invalidCode": "유효하지 않은 코드입니다. 다시 시도하세요.", + "disableTitle": "2FA 비활성화", + "disabledSuccess": "2단계 인증이 비활성화되었습니다.", + "disableFailed": "2FA 비활성화 실패", + "backupTitle": "백업 코드", + "downloadBackup": "백업 코드 다운로드", + "done": "완료" + } +} diff --git a/plugins/totp/locales/translated/nl_NL.json b/plugins/totp/locales/translated/nl_NL.json new file mode 100644 index 000000000..fe071bc09 --- /dev/null +++ b/plugins/totp/locales/translated/nl_NL.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "Voer de verificatiecode in.", + "verify": "Code verifiëren" + }, + "enrollment": { + "title": "TOTP-authenticator", + "on": "Tweefactorauthenticatie (2FA) is ingeschakeld.", + "off": "Voeg extra inlogbeveiliging toe.", + "enable": "Inschakelen", + "disable": "Uitzetten", + "addDevice": "Apparaat toevoegen", + "addTitle": "Authenticatorapparaat toevoegen", + "addScanInstructions": "Scan deze QR-code met de extra authenticatie-app. Bestaande authenticatie-apps blijven gewoon werken.", + "addSuccess": "Extra authenticatie-app geregistreerd", + "addFailed": "Authenticator toevoegen mislukt", + "setupTitle": "TOTP instellen", + "setupFailed": "Het starten van de TOTP-configuratie is mislukt.", + "qrCode": "QR-code", + "secretCopied": "Geheim gekopieerd naar het klembord", + "continueVerify": "Doorgaan met verifiëren", + "verifyTitle": "Code verifiëren", + "verify": "Verifiëren", + "enter6Digits": "Voer een 6-cijferige code in", + "enabledSuccess": "Tweefactorauthenticatie ingeschakeld", + "invalidCode": "Ongeldige code, probeer het opnieuw.", + "disableTitle": "Schakel 2FA uit", + "disabledSuccess": "Tweefactorauthenticatie uitgeschakeld", + "disableFailed": "Het uitschakelen van 2FA is mislukt.", + "backupTitle": "Back-upcodes", + "downloadBackup": "Back-upcodes downloaden", + "done": "Klaar" + } +} diff --git a/plugins/totp/locales/translated/no_NO.json b/plugins/totp/locales/translated/no_NO.json new file mode 100644 index 000000000..20207bc52 --- /dev/null +++ b/plugins/totp/locales/translated/no_NO.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "Skriv inn bekreftelseskoden", + "verify": "Bekreft kode" + }, + "enrollment": { + "title": "TOTP-autentisering", + "on": "2FA er aktivert", + "off": "Legg til ekstra sikkerhet for pålogging", + "enable": "Aktiver", + "disable": "Deaktiver", + "addDevice": "Legg til enhet", + "addTitle": "Legg til autentiseringsenhet", + "addScanInstructions": "Skann denne QR-koden med den ekstra autentiseringsfunksjonen. Eksisterende autentiseringsfunksjoner vil fortsette å fungere.", + "addSuccess": "Ekstra autentiseringsfunksjon registrert", + "addFailed": "Kunne ikke legge til autentiseringsfunksjonen", + "setupTitle": "Konfigurer TOTP", + "setupFailed": "Kunne ikke starte TOTP-oppsettet", + "qrCode": "QR-kode", + "secretCopied": "Hemmelighet kopiert til utklippstavlen", + "continueVerify": "Fortsett å bekrefte", + "verifyTitle": "Bekreft kode", + "verify": "Verifisere", + "enter6Digits": "Skriv inn en 6-sifret kode", + "enabledSuccess": "Tofaktorautentisering aktivert", + "invalidCode": "Ugyldig kode, prøv på nytt", + "disableTitle": "Deaktiver 2FA", + "disabledSuccess": "Tofaktorautentisering deaktivert", + "disableFailed": "Kunne ikke deaktivere 2FA", + "backupTitle": "Sikkerhetskopieringskoder", + "downloadBackup": "Last ned sikkerhetskoder", + "done": "Ferdig" + } +} diff --git a/plugins/totp/locales/translated/pl_PL.json b/plugins/totp/locales/translated/pl_PL.json new file mode 100644 index 000000000..90f5dce6d --- /dev/null +++ b/plugins/totp/locales/translated/pl_PL.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "Wprowadź kod weryfikacyjny", + "verify": "Zweryfikuj kod" + }, + "enrollment": { + "title": "Aplikacja TOTP", + "on": "Włączono uwierzytelnianie dwuskładnikowe", + "off": "Dodaj dodatkowe zabezpieczenia logowania", + "enable": "Włącz", + "disable": "Wyłącz", + "addDevice": "Dodaj urządzenie", + "addTitle": "Dodaj urządzenie uwierzytelniające", + "addScanInstructions": "Zeskanuj ten kod QR za pomocą dodatkowego uwierzytelniacza. Istniejące uwierzytelniacze będą nadal działać.", + "addSuccess": "Zarejestrowano dodatkowego uwierzytelniacza", + "addFailed": "Nie udało się dodać uwierzytelniacza", + "setupTitle": "Skonfiguruj TOTP", + "setupFailed": "Nie udało się uruchomić konfiguracji TOTP", + "qrCode": "Kod QR", + "secretCopied": "Skopiowano sekret do schowka", + "continueVerify": "Kontynuuj weryfikację", + "verifyTitle": "Zweryfikuj kod", + "verify": "Zweryfikuj", + "enter6Digits": "Wprowadź 6-cyfrowy kod", + "enabledSuccess": "Włączono uwierzytelnianie dwuskładnikowe", + "invalidCode": "Nieprawidłowy kod, spróbuj ponownie", + "disableTitle": "Wyłącz 2FA", + "disabledSuccess": "Wyłączono uwierzytelnianie dwuskładnikowe", + "disableFailed": "Nie udało się wyłączyć uwierzytelniania dwuskładnikowego", + "backupTitle": "Kody zapasowe", + "downloadBackup": "Pobierz kody zapasowe", + "done": "Gotowe" + } +} diff --git a/plugins/totp/locales/translated/pt_BR.json b/plugins/totp/locales/translated/pt_BR.json new file mode 100644 index 000000000..75537c0e1 --- /dev/null +++ b/plugins/totp/locales/translated/pt_BR.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "Insira o código de verificação", + "verify": "Código de verificação" + }, + "enrollment": { + "title": "Autenticador TOTP", + "on": "A autenticação de dois fatores está ativada.", + "off": "Adicione segurança extra ao login.", + "enable": "Habilitar", + "disable": "Desativar", + "addDevice": "Adicionar dispositivo", + "addTitle": "Adicionar dispositivo autenticador", + "addScanInstructions": "Escaneie este código QR com o autenticador adicional. Os autenticadores existentes continuarão funcionando.", + "addSuccess": "Autenticador adicional cadastrado", + "addFailed": "Falha ao adicionar autenticador", + "setupTitle": "Configurar TOTP", + "setupFailed": "Falha ao iniciar a configuração TOTP", + "qrCode": "Código QR", + "secretCopied": "Segredo copiado para a área de transferência", + "continueVerify": "Continuar a verificar", + "verifyTitle": "Código de verificação", + "verify": "Verificar", + "enter6Digits": "Digite um código de 6 dígitos", + "enabledSuccess": "Autenticação de dois fatores ativada", + "invalidCode": "Código inválido, tente novamente.", + "disableTitle": "Desative a autenticação de dois fatores (2FA).", + "disabledSuccess": "Autenticação de dois fatores desativada", + "disableFailed": "Falha ao desativar a autenticação de dois fatores (2FA).", + "backupTitle": "Códigos de backup", + "downloadBackup": "Baixar códigos de backup", + "done": "Feito" + } +} diff --git a/plugins/totp/locales/translated/pt_PT.json b/plugins/totp/locales/translated/pt_PT.json new file mode 100644 index 000000000..c07dce095 --- /dev/null +++ b/plugins/totp/locales/translated/pt_PT.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "Introduza o código de verificação", + "verify": "Verificar código" + }, + "enrollment": { + "title": "Autenticador TOTP", + "on": "2FA está ativada", + "off": "Adicionar segurança extra de início de sessão", + "enable": "Ativar", + "disable": "Desativar", + "addDevice": "Adicionar dispositivo", + "addTitle": "Adicionar dispositivo autenticador", + "addScanInstructions": "Digitalize este código QR com o autenticador adicional. Os autenticadores existentes continuarão a funcionar.", + "addSuccess": "Autenticador adicional registado", + "addFailed": "Falha ao adicionar autenticador", + "setupTitle": "Configurar TOTP", + "setupFailed": "Falha ao iniciar a configuração de TOTP", + "qrCode": "Código QR", + "secretCopied": "Segredo copiado para a área de transferência", + "continueVerify": "Continuar para Verificação", + "verifyTitle": "Verificar Código", + "verify": "Verificar", + "enter6Digits": "Insira um código de 6 dígitos", + "enabledSuccess": "Autenticação de dois fatores ativada", + "invalidCode": "Código inválido, tente novamente", + "disableTitle": "Desativar 2FA", + "disabledSuccess": "Autenticação de dois fatores desativada", + "disableFailed": "Falha ao desativar 2FA", + "backupTitle": "Códigos de Backup", + "downloadBackup": "Descarregar Códigos de Backup", + "done": "Concluído" + } +} diff --git a/plugins/totp/locales/translated/ro_RO.json b/plugins/totp/locales/translated/ro_RO.json new file mode 100644 index 000000000..dfb61dd8a --- /dev/null +++ b/plugins/totp/locales/translated/ro_RO.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "Introduceți codul de verificare", + "verify": "Verificați codul" + }, + "enrollment": { + "title": "Autentificator TOTP", + "on": "2FA este activat", + "off": "Adăugați securitate suplimentară la conectare", + "enable": "Permite", + "disable": "Dezactivare", + "addDevice": "Adăugați dispozitiv", + "addTitle": "Adăugați un dispozitiv de autentificare", + "addScanInstructions": "Scanați acest cod QR cu ajutorul autentificatorului suplimentar. Autentificatoarele existente vor continua să funcționeze.", + "addSuccess": "Autentificator suplimentar înscris", + "addFailed": "Nu s-a putut adăuga autentificatorul", + "setupTitle": "Configurați TOTP", + "setupFailed": "Nu s-a putut porni configurarea TOTP", + "qrCode": "Cod QR", + "secretCopied": "Secret copiat în clipboard", + "continueVerify": "Continuați verificarea", + "verifyTitle": "Verificați codul", + "verify": "Verifica", + "enter6Digits": "Introduceți un cod de 6 cifre", + "enabledSuccess": "Autentificarea cu doi factori activată", + "invalidCode": "Cod nevalid, vă rugăm să încercați din nou", + "disableTitle": "Dezactivați 2FA", + "disabledSuccess": "Autentificarea cu doi factori este dezactivată", + "disableFailed": "Nu s-a putut dezactiva 2FA", + "backupTitle": "Coduri de rezervă", + "downloadBackup": "Descărcați coduri de rezervă", + "done": "Făcut" + } +} diff --git a/plugins/totp/locales/translated/ru_RU.json b/plugins/totp/locales/translated/ru_RU.json new file mode 100644 index 000000000..c12fd45c6 --- /dev/null +++ b/plugins/totp/locales/translated/ru_RU.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "Введите проверочный код", + "verify": "Проверить код" + }, + "enrollment": { + "title": "Аутентификатор TOTP", + "on": "2FA включена", + "off": "Добавить дополнительную защиту", + "enable": "Включить", + "disable": "Отключить", + "addDevice": "Добавить устройство", + "addTitle": "Добавить устройство аутентификации", + "addScanInstructions": "Отсканируйте этот QR-код с помощью дополнительного аутентификатора. Существующие аутентификаторы продолжат работать.", + "addSuccess": "Зарегистрирован дополнительный аутентификатор.", + "addFailed": "Не удалось добавить аутентификатор.", + "setupTitle": "Настроить TOTP", + "setupFailed": "Не удалось запустить настройку TOTP", + "qrCode": "QR-код", + "secretCopied": "Секрет скопирован в буфер обмена", + "continueVerify": "Продолжить проверку", + "verifyTitle": "Проверка кода", + "verify": "Проверить", + "enter6Digits": "Введите 6-значный код", + "enabledSuccess": "Двухфакторная аутентификация включена", + "invalidCode": "Неверный код, попробуйте снова", + "disableTitle": "Отключить 2FA", + "disabledSuccess": "Двухфакторная аутентификация отключена", + "disableFailed": "Не удалось отключить 2FA", + "backupTitle": "Резервные коды", + "downloadBackup": "Скачать резервные коды", + "done": "Готово" + } +} diff --git a/plugins/totp/locales/translated/sr_SP.json b/plugins/totp/locales/translated/sr_SP.json new file mode 100644 index 000000000..cadd75c77 --- /dev/null +++ b/plugins/totp/locales/translated/sr_SP.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "Унесите верификациони код", + "verify": "Верификујте код" + }, + "enrollment": { + "title": "TOTP аутентификатор", + "on": "2FA је омогућена", + "off": "Додајте додатну безбедност пријављивања", + "enable": "Омогући", + "disable": "Онемогући", + "addDevice": "Додај уређај", + "addTitle": "Додај уређај за аутентификовање", + "addScanInstructions": "Скенирајте овај QR код додатним аутентификатором. Постојећи аутентификатори ће наставити да раде.", + "addSuccess": "Додатни аутентификатор је регистрован", + "addFailed": "Додавање аутентификатора није успело", + "setupTitle": "Подешавање TOTP-а", + "setupFailed": "Покретање подешавања TOTP-а није успело", + "qrCode": "QR код", + "secretCopied": "Тајни токен је копиран у међуспремник", + "continueVerify": "Настави са верификацијом", + "verifyTitle": "Верификујте код", + "verify": "Верификуј", + "enter6Digits": "Унесите шестоцифрени код", + "enabledSuccess": "Омогућена је двофакторска аутентификација", + "invalidCode": "Неважећи код, покушајте поново", + "disableTitle": "Онемогући 2FA", + "disabledSuccess": "Двофакторска аутентификација је онемогућена", + "disableFailed": "Онемогућавање 2FA није успело", + "backupTitle": "Резервни кодови", + "downloadBackup": "Преузмите резервне кодове", + "done": "Готово" + } +} diff --git a/plugins/totp/locales/translated/sv_SE.json b/plugins/totp/locales/translated/sv_SE.json new file mode 100644 index 000000000..1e7551cb4 --- /dev/null +++ b/plugins/totp/locales/translated/sv_SE.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "Ange verifieringskod", + "verify": "Verifiera kod" + }, + "enrollment": { + "title": "TOTP-autentiserare", + "on": "2FA är aktiverat", + "off": "Lägg till extra inloggningssäkerhet", + "enable": "Aktivera", + "disable": "Inaktivera", + "addDevice": "Lägg till enhet", + "addTitle": "Lägg till autentiseringsenhet", + "addScanInstructions": "Skanna den här QR-koden med den extra autentiseringsfunktionen. Befintliga autentiseringsfunktioner kommer att fortsätta fungera.", + "addSuccess": "Ytterligare autentiserare registrerad", + "addFailed": "Misslyckades med att lägga till autentiserare", + "setupTitle": "Konfigurera TOTP", + "setupFailed": "Misslyckades med att starta TOTP-installationen", + "qrCode": "QR-kod", + "secretCopied": "Hemlighet kopierad till urklipp", + "continueVerify": "Fortsätt att verifiera", + "verifyTitle": "Verifiera kod", + "verify": "Kontrollera", + "enter6Digits": "Ange en 6-siffrig kod", + "enabledSuccess": "Tvåfaktorsautentisering aktiverad", + "invalidCode": "Ogiltig kod, försök igen", + "disableTitle": "Inaktivera 2FA", + "disabledSuccess": "Tvåfaktorsautentisering inaktiverad", + "disableFailed": "Misslyckades med att inaktivera 2FA", + "backupTitle": "Reservkoder", + "downloadBackup": "Ladda ner reservkoder", + "done": "Gjort" + } +} diff --git a/plugins/totp/locales/translated/th_TH.json b/plugins/totp/locales/translated/th_TH.json new file mode 100644 index 000000000..ff217013e --- /dev/null +++ b/plugins/totp/locales/translated/th_TH.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "ป้อนรหัสยืนยัน", + "verify": "ยืนยันรหัส" + }, + "enrollment": { + "title": "ตัวตรวจสอบสิทธิ์ TOTP", + "on": "เปิดใช้งาน 2FA แล้ว", + "off": "เพิ่มระบบรักษาความปลอดภัยในการเข้าสู่ระบบเพิ่มเติม", + "enable": "เปิดใช้งาน", + "disable": "ปิดใช้งาน", + "addDevice": "เพิ่มอุปกรณ์", + "addTitle": "เพิ่มอุปกรณ์ยืนยันตัวตน", + "addScanInstructions": "สแกนคิวอาร์โค้ดนี้ด้วยอุปกรณ์ยืนยันตัวตนเพิ่มเติม อุปกรณ์ยืนยันตัวตนที่มีอยู่เดิมยังคงใช้งานได้ต่อไป", + "addSuccess": "ลงทะเบียนตัวยืนยันตัวตนเพิ่มเติมแล้ว", + "addFailed": "ไม่สามารถเพิ่มตัวตรวจสอบสิทธิ์ได้", + "setupTitle": "ตั้งค่า TOTP", + "setupFailed": "ไม่สามารถเริ่มการตั้งค่า TOTP ได้", + "qrCode": "รหัส QR", + "secretCopied": "คัดลอกข้อมูลลับไปยังคลิปบอร์ดแล้ว", + "continueVerify": "ดำเนินการตรวจสอบต่อไป", + "verifyTitle": "ยืนยันรหัส", + "verify": "ตรวจสอบ", + "enter6Digits": "ป้อนรหัส 6 หลัก", + "enabledSuccess": "เปิดใช้งานการตรวจสอบสิทธิ์แบบสองขั้นตอน", + "invalidCode": "รหัสไม่ถูกต้อง โปรดลองอีกครั้ง", + "disableTitle": "ปิดใช้งาน 2FA", + "disabledSuccess": "ปิดใช้งานการตรวจสอบสิทธิ์แบบสองขั้นตอน", + "disableFailed": "ไม่สามารถปิดใช้งาน 2FA ได้", + "backupTitle": "รหัสสำรอง", + "downloadBackup": "ดาวน์โหลดรหัสสำรอง", + "done": "เสร็จแล้ว" + } +} diff --git a/plugins/totp/locales/translated/tr_TR.json b/plugins/totp/locales/translated/tr_TR.json new file mode 100644 index 000000000..096df60e3 --- /dev/null +++ b/plugins/totp/locales/translated/tr_TR.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "Doğrulama kodunu girin", + "verify": "Kodu Doğrula" + }, + "enrollment": { + "title": "TOTP Kimlik Doğrulayıcı", + "on": "2FA etkin", + "off": "Ek giriş güvenliği ekleyin", + "enable": "Etkinleştir", + "disable": "Devre Dışı Bırak", + "addDevice": "Cihaz ekle", + "addTitle": "Kimlik doğrulama cihazı ekle", + "addScanInstructions": "Bu QR kodunu ek kimlik doğrulayıcı ile tarayın. Mevcut kimlik doğrulayıcılar çalışmaya devam edecektir.", + "addSuccess": "Ek kimlik doğrulayıcı kaydedildi.", + "addFailed": "Kimlik doğrulayıcı eklenemedi.", + "setupTitle": "TOTP Kurulumu", + "setupFailed": "TOTP kurulumu başlatılamadı", + "qrCode": "QR Kodu", + "secretCopied": "Gizli anahtar panoya kopyalandı", + "continueVerify": "Doğrulamaya Devam Et", + "verifyTitle": "Kodu Doğrula", + "verify": "Doğrula", + "enter6Digits": "6 haneli bir kod girin", + "enabledSuccess": "İki faktörlü kimlik doğrulama etkinleştirildi", + "invalidCode": "Geçersiz kod, lütfen tekrar deneyin", + "disableTitle": "2FA'yı Devre Dışı Bırak", + "disabledSuccess": "İki faktörlü kimlik doğrulama devre dışı bırakıldı", + "disableFailed": "2FA devre dışı bırakılamadı", + "backupTitle": "Yedek Kodlar", + "downloadBackup": "Yedek Kodları İndir", + "done": "Tamam" + } +} diff --git a/plugins/totp/locales/translated/uk_UA.json b/plugins/totp/locales/translated/uk_UA.json new file mode 100644 index 000000000..efc50df89 --- /dev/null +++ b/plugins/totp/locales/translated/uk_UA.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "Введіть код підтвердження", + "verify": "Підтвердити код" + }, + "enrollment": { + "title": "Аутентифікатор TOTP", + "on": "Двофакторна аутентифікація (2FA) увімкнена", + "off": "Додайте додаткову безпеку входу", + "enable": "Увімкнути", + "disable": "Вимкнути", + "addDevice": "Додати пристрій", + "addTitle": "Додати пристрій автентифікатора", + "addScanInstructions": "Відскануйте цей QR-код за допомогою додаткового автентифікатора. Існуючі автентифікатори продовжуватимуть працювати.", + "addSuccess": "Зареєстровано додатковий автентифікатор", + "addFailed": "Не вдалося додати автентифікатор", + "setupTitle": "Налаштування TOTP", + "setupFailed": "Не вдалося розпочати налаштування TOTP", + "qrCode": "QR-код", + "secretCopied": "Секрет скопійовано в буфер обміну", + "continueVerify": "Продовжити перевірку", + "verifyTitle": "Підтвердити код", + "verify": "Перевірити", + "enter6Digits": "Введіть 6-значний код", + "enabledSuccess": "Двофакторна автентифікація ввімкнена", + "invalidCode": "Недійсний код, спробуйте ще раз", + "disableTitle": "Вимкнути 2FA", + "disabledSuccess": "Двофакторну автентифікацію вимкнено", + "disableFailed": "Не вдалося вимкнути 2FA", + "backupTitle": "Резервні коди", + "downloadBackup": "Завантажити резервні коди", + "done": "Готово" + } +} diff --git a/plugins/totp/locales/translated/vi_VN.json b/plugins/totp/locales/translated/vi_VN.json new file mode 100644 index 000000000..6bcfca0ec --- /dev/null +++ b/plugins/totp/locales/translated/vi_VN.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "Nhập mã xác minh", + "verify": "Xác minh mã" + }, + "enrollment": { + "title": "Trình xác thực TOTP", + "on": "2FA đã được bật", + "off": "Thêm bảo mật đăng nhập bổ sung", + "enable": "Bật", + "disable": "Tắt", + "addDevice": "Thêm thiết bị", + "addTitle": "Thêm thiết bị xác thực", + "addScanInstructions": "Quét mã QR này bằng thiết bị xác thực bổ sung. Các thiết bị xác thực hiện có vẫn sẽ hoạt động bình thường.", + "addSuccess": "Đã đăng ký thêm trình xác thực.", + "addFailed": "Thêm trình xác thực không thành công", + "setupTitle": "Thiết lập TOTP", + "setupFailed": "Không khởi động được cài đặt TOTP", + "qrCode": "Mã QR", + "secretCopied": "Đã sao chép mã bí mật vào clipboard", + "continueVerify": "Tiếp tục xác minh", + "verifyTitle": "Xác minh mã", + "verify": "Xác minh", + "enter6Digits": "Nhập mã 6 chữ số", + "enabledSuccess": "Đã bật xác thực hai yếu tố", + "invalidCode": "Mã không hợp lệ, vui lòng thử lại", + "disableTitle": "Tắt 2FA", + "disabledSuccess": "Đã tắt xác thực hai yếu tố", + "disableFailed": "Không tắt được 2FA", + "backupTitle": "Mã dự phòng", + "downloadBackup": "Tải mã dự phòng", + "done": "Xong" + } +} diff --git a/plugins/totp/locales/translated/zh_CN.json b/plugins/totp/locales/translated/zh_CN.json new file mode 100644 index 000000000..b711de57a --- /dev/null +++ b/plugins/totp/locales/translated/zh_CN.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "输入验证码", + "verify": "验证" + }, + "enrollment": { + "title": "TOTP 身份验证器", + "on": "双因素认证已启用", + "off": "添加额外的登录安全", + "enable": "启用", + "disable": "禁用", + "addDevice": "添加设备", + "addTitle": "添加身份验证器设备", + "addScanInstructions": "请使用新增的验证器扫描此二维码。现有验证器将继续有效。", + "addSuccess": "已注册其他身份验证器", + "addFailed": "添加身份验证器失败", + "setupTitle": "设置 TOTP", + "setupFailed": "启动 TOTP 设置失败", + "qrCode": "二维码", + "secretCopied": "密钥已复制到剪贴板", + "continueVerify": "继续验证", + "verifyTitle": "验证验证码", + "verify": "验证", + "enter6Digits": "输入 6 位验证码", + "enabledSuccess": "双因素认证已启用", + "invalidCode": "验证码无效,请重试", + "disableTitle": "禁用 2FA", + "disabledSuccess": "双因素认证已禁用", + "disableFailed": "禁用 2FA 失败", + "backupTitle": "备份验证码", + "downloadBackup": "下载备份验证码", + "done": "完成" + } +} diff --git a/plugins/totp/locales/translated/zh_TW.json b/plugins/totp/locales/translated/zh_TW.json new file mode 100644 index 000000000..ce6d1b9b3 --- /dev/null +++ b/plugins/totp/locales/translated/zh_TW.json @@ -0,0 +1,34 @@ +{ + "challenge": { + "enterCode": "輸入驗證碼", + "verify": "驗證代碼" + }, + "enrollment": { + "title": "TOTP 驗證器", + "on": "雙因素驗證已啟用", + "off": "新增額外的登入安全性", + "enable": "啟用", + "disable": "停用", + "addDevice": "新增設備", + "addTitle": "新增身份驗證器設備", + "addScanInstructions": "請使用新增的驗證器掃描此二維碼。現有驗證器將繼續有效。", + "addSuccess": "已註冊其他身份驗證器", + "addFailed": "新增身份驗證器失敗", + "setupTitle": "設定 TOTP", + "setupFailed": "無法啟動 TOTP 設定", + "qrCode": "QR 碼", + "secretCopied": "密鑰已複製到剪貼簿", + "continueVerify": "繼續驗證", + "verifyTitle": "驗證碼", + "verify": "驗證", + "enter6Digits": "輸入 6 位數驗證碼", + "enabledSuccess": "雙因素驗證已啟用", + "invalidCode": "驗證碼無效,請再試一次", + "disableTitle": "停用雙因素驗證", + "disabledSuccess": "雙因素驗證已停用", + "disableFailed": "無法停用雙因素驗證", + "backupTitle": "備用驗證碼", + "downloadBackup": "下載備用驗證碼", + "done": "完成" + } +} diff --git a/plugins/totp/manifest.json b/plugins/totp/manifest.json new file mode 100644 index 000000000..d935fff24 --- /dev/null +++ b/plugins/totp/manifest.json @@ -0,0 +1,31 @@ +{ + "id": "totp", + "name": "TOTP", + "version": "1.0.0", + "description": "Ask for a code from an authenticator app after signing in, with backup codes for when the phone is not at hand.", + "author": { + "name": "Termix" + }, + "license": "MIT", + "repository": "https://github.com/Termix-SSH/Termix", + "category": "Access & Security", + "icon": "ShieldCheck", + "engine": { + "termix": ">=2.9.0", + "api": "1" + }, + "capabilities": [ + "db:own", + "secrets:own", + "network:serve", + "auth:provide", + "ui:surface" + ], + "contributes": { + "auth": { + "secondFactors": ["totp"] + } + }, + "locales": "locales", + "platforms": ["linux", "win32", "darwin"] +} diff --git a/plugins/totp/migrations/mysql/0001_create_enrollments.sql b/plugins/totp/migrations/mysql/0001_create_enrollments.sql new file mode 100644 index 000000000..d68cb9523 --- /dev/null +++ b/plugins/totp/migrations/mysql/0001_create_enrollments.sql @@ -0,0 +1,12 @@ +-- totp 0001: create_enrollments +-- Generated by termix-plugin migrations. Review before committing. + +CREATE TABLE IF NOT EXISTS `p_totp_enrollments` ( + `user_id` varchar(255) PRIMARY KEY NOT NULL, + `secret` text, + `pending_secret` text, + `backup_codes` text, + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +); diff --git a/plugins/totp/migrations/postgres/0001_create_enrollments.sql b/plugins/totp/migrations/postgres/0001_create_enrollments.sql new file mode 100644 index 000000000..dd6d2ca16 --- /dev/null +++ b/plugins/totp/migrations/postgres/0001_create_enrollments.sql @@ -0,0 +1,12 @@ +-- totp 0001: create_enrollments +-- Generated by termix-plugin migrations. Review before committing. + +CREATE TABLE IF NOT EXISTS "p_totp_enrollments" ( + "user_id" varchar(255) PRIMARY KEY NOT NULL, + "secret" text, + "pending_secret" text, + "backup_codes" text, + "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE +); diff --git a/plugins/totp/migrations/snapshot.json b/plugins/totp/migrations/snapshot.json new file mode 100644 index 000000000..daf57900d --- /dev/null +++ b/plugins/totp/migrations/snapshot.json @@ -0,0 +1,35 @@ +{ + "version": 1, + "tables": { + "enrollments": { + "columns": { + "userId": { + "type": "refUser", + "length": 255, + "notNull": true, + "primaryKey": true + }, + "secret": { + "type": "encryptedText" + }, + "pendingSecret": { + "type": "encryptedText" + }, + "backupCodes": { + "type": "encryptedText" + }, + "createdAt": { + "type": "timestamp", + "notNull": true, + "defaultNow": true + }, + "updatedAt": { + "type": "timestamp", + "notNull": true, + "defaultNow": true + } + }, + "indexes": [] + } + } +} diff --git a/plugins/totp/migrations/sqlite/0001_create_enrollments.sql b/plugins/totp/migrations/sqlite/0001_create_enrollments.sql new file mode 100644 index 000000000..2bc7364f6 --- /dev/null +++ b/plugins/totp/migrations/sqlite/0001_create_enrollments.sql @@ -0,0 +1,12 @@ +-- totp 0001: create_enrollments +-- Generated by termix-plugin migrations. Review before committing. + +CREATE TABLE IF NOT EXISTS "p_totp_enrollments" ( + "user_id" text PRIMARY KEY NOT NULL, + "secret" text, + "pending_secret" text, + "backup_codes" text, + "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE +); diff --git a/plugins/totp/package.json b/plugins/totp/package.json new file mode 100644 index 000000000..e1b239a68 --- /dev/null +++ b/plugins/totp/package.json @@ -0,0 +1,21 @@ +{ + "name": "@termix-plugin/totp", + "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": { + "qrcode": "^1.5.4", + "speakeasy": "^2.0.0" + }, + "devDependencies": { + "@termix/plugin-sdk": "*", + "@types/qrcode": "^1.5.6", + "@types/speakeasy": "^2.0.10" + } +} diff --git a/plugins/totp/src/backend/index.ts b/plugins/totp/src/backend/index.ts new file mode 100644 index 000000000..569cbb21f --- /dev/null +++ b/plugins/totp/src/backend/index.ts @@ -0,0 +1,41 @@ +import type { Router } from "express"; +import type { PluginContext } from "@termix/plugin-sdk/backend"; +import { enrollments } from "./tables.js"; +import { createEnrollmentRepository } from "./repository.js"; +import { createTotpService, FACTOR_ID, normalizeCode } from "./totp.js"; +import { registerTotpRoutes } from "./routes.js"; + +export async function activate(ctx: PluginContext) { + const table = await ctx.db.define(enrollments); + const totp = createTotpService( + ctx, + createEnrollmentRepository(ctx.db, table), + ); + + ctx.auth.registerSecondFactor({ + id: FACTOR_ID, + labelKey: "factor", + isEnrolled: (userId) => totp.isEnrolled(userId), + verify: async (userId, body) => { + const result = await totp.checkCode( + userId, + normalizeCode(body.totp_code ?? body.code), + ); + if (result === null) { + return { + ok: false, + error: + "Your authenticator could not be read. Contact an admin to reset your second factor.", + }; + } + return result; + }, + reset: (userId) => totp.remove(userId), + }); + + registerTotpRoutes(ctx.http.router(), ctx, totp); +} + +export async function deactivate() { + // Everything above was registered through ctx and is disposed by core. +} diff --git a/plugins/totp/src/backend/repository.ts b/plugins/totp/src/backend/repository.ts new file mode 100644 index 000000000..8209bf923 --- /dev/null +++ b/plugins/totp/src/backend/repository.ts @@ -0,0 +1,69 @@ +import { eq } from "drizzle-orm"; +import type { PluginDatabase } from "@termix/plugin-sdk/backend"; + +export interface EnrollmentRecord { + userId: string; + secret: string | null; + pendingSecret: string | null; + backupCodes: string | null; + createdAt: string; + updatedAt: string; +} + +/* 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 */ + +export type EnrollmentRepository = ReturnType< + typeof createEnrollmentRepository +>; + +/** Sealed values only: nothing here ever sees a plaintext secret. */ +export function createEnrollmentRepository(db: PluginDatabase, table: Table) { + const client = () => db.client(); + + async function find(userId: string): Promise { + const drizzle = await client(); + const rows = await drizzle + .select() + .from(table) + .where(eq(table.userId, userId)) + .limit(1); + return (rows[0] as EnrollmentRecord) ?? null; + } + + async function save( + userId: string, + values: Partial< + Pick + >, + ): Promise { + const drizzle = await client(); + const now = new Date().toISOString(); + if (await find(userId)) { + await drizzle + .update(table) + .set({ ...values, updatedAt: now }) + .where(eq(table.userId, userId)); + } else { + await drizzle + .insert(table) + .values({ userId, ...values, createdAt: now, updatedAt: now }); + } + await db.persist(); + } + + return { + find, + save, + + async remove(userId: string): Promise { + const drizzle = await client(); + await drizzle.delete(table).where(eq(table.userId, userId)); + await db.persist(); + }, + }; +} diff --git a/plugins/totp/src/backend/routes.ts b/plugins/totp/src/backend/routes.ts new file mode 100644 index 000000000..b1b7d70a4 --- /dev/null +++ b/plugins/totp/src/backend/routes.ts @@ -0,0 +1,268 @@ +import type { Request, Response, Router } from "express"; +import { eq } from "drizzle-orm"; +import QRCode from "qrcode"; +import speakeasy from "speakeasy"; +import { + LoginMethodError, + type PluginContext, +} from "@termix/plugin-sdk/backend"; +import { FACTOR_ID, normalizeCode, type TotpService } from "./totp.js"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +type Table = any; +type Drizzle = any; +/* eslint-enable @typescript-eslint/no-explicit-any */ + +export function registerTotpRoutes( + router: Router, + ctx: PluginContext, + totp: TotpService, +): void { + const actor = () => ctx.currentActor()!; + + async function accountLabel(userId: string): Promise { + try { + const { users } = await ctx.db.refs<{ users: Table }>(); + const drizzle = await ctx.db.client(); + const rows = await drizzle + .select({ username: users.username }) + .from(users) + .where(eq(users.id, userId)) + .limit(1); + if (rows[0]?.username) return `Termix (${rows[0].username})`; + } catch { + // The label is cosmetic. + } + return "Termix"; + } + + const fail = (res: Response, message: string, error: unknown) => { + ctx.log.error( + message, + error instanceof Error ? error : new Error(String(error)), + ); + res.status(500).json({ error: message }); + }; + + /** + * @openapi + * /plugin-api/totp/status: + * get: + * summary: TOTP status for the caller + * tags: + * - TOTP + * responses: + * 200: + * description: Whether the caller has TOTP on. + */ + router.get("/status", async (_req: Request, res: Response) => { + try { + res.json({ enabled: await totp.isEnrolled(actor()) }); + } catch (error) { + fail(res, "Failed to read TOTP status", error); + } + }); + + /** + * @openapi + * /plugin-api/totp/setup: + * post: + * summary: Start TOTP setup + * description: Returns a new secret and QR code. With TOTP already on, a current code or backup code in `credential` returns the existing secret instead, to add another authenticator. + * tags: + * - TOTP + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * credential: + * type: string + * responses: + * 200: + * description: Secret and QR code. + * 400: + * description: A code is required. + * 401: + * description: Invalid code. + */ + router.post("/setup", async (req: Request, res: Response) => { + const userId = actor(); + try { + const label = await accountLabel(userId); + if (await totp.isEnrolled(userId)) { + const credential = normalizeCode(req.body?.credential); + if (!credential) { + return res.status(400).json({ error: "A TOTP code is required" }); + } + if (!(await totp.checkCode(userId, credential))) { + return res.status(401).json({ error: "Invalid TOTP code" }); + } + const secret = await totp.activeSecret(userId); + if (!secret) { + return res.status(409).json({ error: "TOTP secret is unavailable" }); + } + const url = speakeasy.otpauthURL({ + secret, + label, + encoding: "base32", + }); + return res.json({ + secret, + qr_code: await QRCode.toDataURL(url), + additional: true, + }); + } + + const { secret, otpauthUrl } = await totp.startSetup(userId, label); + res.json({ secret, qr_code: await QRCode.toDataURL(otpauthUrl) }); + } catch (error) { + fail(res, "Failed to setup TOTP", error); + } + }); + + /** + * @openapi + * /plugin-api/totp/enable: + * post: + * summary: Turn TOTP on + * description: Checks a code from the secret /setup returned, turns TOTP on and returns backup codes. Signs the caller out everywhere else. + * tags: + * - TOTP + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * totp_code: + * type: string + * responses: + * 200: + * description: TOTP on, with backup codes. + * 400: + * description: Missing code, already on, or setup not started. + * 401: + * description: Invalid code. + * 409: + * description: Core does not allow second factors right now. + */ + router.post("/enable", async (req: Request, res: Response) => { + const userId = actor(); + const code = normalizeCode(req.body?.totp_code); + if (!code) return res.status(400).json({ error: "TOTP code is required" }); + try { + if (await totp.isEnrolled(userId)) { + return res.status(400).json({ error: "TOTP is already enabled" }); + } + const pending = await totp.pendingSecretFor(userId, code); + if (pending.status === "not-started") { + return res.status(400).json({ error: "TOTP setup not initiated" }); + } + if (pending.status === "invalid") { + return res.status(401).json({ error: "Invalid TOTP code" }); + } + // Core's policy first, so a refusal leaves nothing half enabled. + await ctx.auth.recordEnrollment(userId, FACTOR_ID); + const backupCodes = await totp.activate(userId, pending.secret); + res.json({ + message: "TOTP enabled successfully", + backup_codes: backupCodes, + }); + } catch (error) { + if (error instanceof LoginMethodError) { + return res.status(error.status).json({ error: error.message }); + } + fail(res, "Failed to enable TOTP", error); + } + }); + + /** + * @openapi + * /plugin-api/totp/disable: + * post: + * summary: Turn TOTP off + * tags: + * - TOTP + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * totp_code: + * type: string + * responses: + * 200: + * description: TOTP off. + * 400: + * description: Missing code or TOTP is not on. + * 401: + * description: Invalid code. + */ + router.post("/disable", async (req: Request, res: Response) => { + const userId = actor(); + const code = normalizeCode(req.body?.totp_code); + if (!code) { + return res.status(400).json({ error: "A TOTP code is required" }); + } + try { + const ok = await totp.checkCode(userId, code); + if (ok === null) { + return res.status(400).json({ error: "TOTP is not enabled" }); + } + if (!ok) return res.status(401).json({ error: "Invalid TOTP code" }); + await totp.remove(userId); + await ctx.auth.removeEnrollment(userId, FACTOR_ID); + ctx.log.info(`Two-factor authentication disabled for ${userId}`); + res.json({ message: "TOTP disabled successfully" }); + } catch (error) { + fail(res, "Failed to disable TOTP", error); + } + }); + + /** + * @openapi + * /plugin-api/totp/backup-codes: + * post: + * summary: Replace the backup codes + * tags: + * - TOTP + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * totp_code: + * type: string + * responses: + * 200: + * description: New backup codes. + * 400: + * description: Missing code or TOTP is not on. + * 401: + * description: Invalid code. + */ + router.post("/backup-codes", async (req: Request, res: Response) => { + const userId = actor(); + const code = normalizeCode(req.body?.totp_code); + if (!code) { + return res.status(400).json({ error: "A TOTP code is required" }); + } + try { + const ok = await totp.checkCode(userId, code); + if (ok === null) { + return res.status(400).json({ error: "TOTP is not enabled" }); + } + if (!ok) return res.status(401).json({ error: "Invalid TOTP code" }); + res.json({ backup_codes: await totp.replaceBackupCodes(userId) }); + } catch (error) { + fail(res, "Failed to generate backup codes", error); + } + }); +} diff --git a/plugins/totp/src/backend/tables.ts b/plugins/totp/src/backend/tables.ts new file mode 100644 index 000000000..ec83f737c --- /dev/null +++ b/plugins/totp/src/backend/tables.ts @@ -0,0 +1,24 @@ +import { + defineTable, + encryptedText, + refUser, + timestamp, +} from "@termix/plugin-sdk/db"; + +/** + * One row per user who started or finished setup. The secret columns hold + * values sealed with ctx.secrets.seal, so they can be read during login + * without an acting user. A non-null secret means the user is enrolled. + */ +export const enrollments = defineTable("enrollments", { + userId: refUser().primaryKey(), + secret: encryptedText(), + // Set by /setup, promoted to secret once a code from it verifies. + pendingSecret: encryptedText(), + // A sealed JSON array of the unused backup codes. + backupCodes: encryptedText(), + createdAt: timestamp().notNull().defaultNow(), + updatedAt: timestamp().notNull().defaultNow(), +}); + +export const tables = [enrollments]; diff --git a/plugins/totp/src/backend/totp.ts b/plugins/totp/src/backend/totp.ts new file mode 100644 index 000000000..40b4c653f --- /dev/null +++ b/plugins/totp/src/backend/totp.ts @@ -0,0 +1,136 @@ +import crypto from "node:crypto"; +import speakeasy from "speakeasy"; +import type { PluginContext } from "@termix/plugin-sdk/backend"; +import type { EnrollmentRepository } from "./repository.js"; + +export const FACTOR_ID = "totp"; + +const BACKUP_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; +const BACKUP_CODE_COUNT = 8; + +/** Eight characters from a CSPRNG; Math.random is not fit for secrets. */ +export function generateBackupCode(): string { + let code = ""; + for (let i = 0; i < 8; i++) { + code += BACKUP_CODE_ALPHABET[crypto.randomInt(BACKUP_CODE_ALPHABET.length)]; + } + return code; +} + +export function generateBackupCodes(): string[] { + return Array.from({ length: BACKUP_CODE_COUNT }, () => generateBackupCode()); +} + +export function verifyTotpCode(secret: string, code: string): boolean { + return speakeasy.totp.verify({ + secret, + encoding: "base32", + token: code, + window: 2, + }); +} + +export function normalizeCode(value: unknown): string { + return typeof value === "string" ? value.trim().toUpperCase() : ""; +} + +export type TotpService = ReturnType; + +/** + * Reads and checks a user's enrolment. Secrets are sealed with the + * installation key, so this works during login with no acting user. + */ +export function createTotpService( + ctx: PluginContext, + repository: EnrollmentRepository, +) { + const unseal = (value: string | null) => + value ? ctx.secrets.unseal(value) : Promise.resolve(null); + + async function readBackupCodes(sealed: string | null): Promise { + const raw = await unseal(sealed); + if (!raw) return []; + try { + const codes = JSON.parse(raw); + return Array.isArray(codes) ? codes : []; + } catch { + return []; + } + } + + async function sealBackupCodes(codes: string[]): Promise { + return ctx.secrets.seal(JSON.stringify(codes)); + } + + return { + async isEnrolled(userId: string): Promise { + return !!(await repository.find(userId))?.secret; + }, + + /** The active secret, or null when not enrolled or unreadable. */ + async activeSecret(userId: string): Promise { + return unseal((await repository.find(userId))?.secret ?? null); + }, + + /** + * True for a current code from the authenticator or an unused backup + * code, which is burnt. Null when the user has no readable secret. + */ + async checkCode(userId: string, code: string): Promise { + const row = await repository.find(userId); + const secret = await unseal(row?.secret ?? null); + if (!row || !secret) return null; + if (!code) return false; + if (verifyTotpCode(secret, code)) return true; + + const codes = await readBackupCodes(row.backupCodes); + const index = codes.indexOf(code); + if (index === -1) return false; + codes.splice(index, 1); + await repository.save(userId, { + backupCodes: await sealBackupCodes(codes), + }); + return true; + }, + + /** Starts setup with a new secret, replacing any unfinished one. */ + async startSetup(userId: string, label: string) { + const secret = speakeasy.generateSecret({ name: label, length: 32 }); + await repository.save(userId, { + pendingSecret: await ctx.secrets.seal(secret.base32), + }); + return { secret: secret.base32, otpauthUrl: secret.otpauth_url ?? "" }; + }, + + /** The unfinished setup's secret, when a code from it verifies. */ + async pendingSecretFor(userId: string, code: string) { + const pending = await unseal( + (await repository.find(userId))?.pendingSecret ?? null, + ); + if (!pending) return { status: "not-started" as const }; + if (!verifyTotpCode(pending, code)) return { status: "invalid" as const }; + return { status: "ok" as const, secret: pending }; + }, + + /** Makes the verified pending secret active and issues backup codes. */ + async activate(userId: string, secret: string): Promise { + const codes = generateBackupCodes(); + await repository.save(userId, { + secret: await ctx.secrets.seal(secret), + pendingSecret: null, + backupCodes: await sealBackupCodes(codes), + }); + return codes; + }, + + async replaceBackupCodes(userId: string): Promise { + const codes = generateBackupCodes(); + await repository.save(userId, { + backupCodes: await sealBackupCodes(codes), + }); + return codes; + }, + + remove: (userId: string) => repository.remove(userId), + }; +} diff --git a/plugins/totp/src/frontend/TotpChallenge.tsx b/plugins/totp/src/frontend/TotpChallenge.tsx new file mode 100644 index 000000000..ea92d66d6 --- /dev/null +++ b/plugins/totp/src/frontend/TotpChallenge.tsx @@ -0,0 +1,82 @@ +import { useEffect, useRef, useState } from "react"; +import { toast } from "sonner"; +import { Button, Input } from "@termix/plugin-sdk/ui"; +import { + useTranslation, + type SecondFactorUIProps, +} from "@termix/plugin-sdk/frontend"; +import { isValidTotpInput, normalizeTotpInput } from "./totp-input"; + +const primaryButton = + "w-full bg-accent-brand hover:bg-accent-brand/90 text-background font-bold"; + +export function TotpChallenge({ + disabled, + verify, + cancel, +}: SecondFactorUIProps) { + const { t } = useTranslation(); + const [code, setCode] = useState(""); + const [loading, setLoading] = useState(false); + const inputRef = useRef(null); + + useEffect(() => { + inputRef.current?.focus(); + }, []); + + return ( +
{ + event.preventDefault(); + if (!isValidTotpInput(code)) { + toast.error(t("challenge.enterCode")); + return; + } + setLoading(true); + try { + await verify({ totp_code: code }); + } finally { + setLoading(false); + } + }} + className="flex flex-col gap-4" + > + +

+ {t("challenge.backupCodeHint")} +

+ + +
+ ); +} diff --git a/plugins/totp/src/frontend/TotpEnrollment.tsx b/plugins/totp/src/frontend/TotpEnrollment.tsx new file mode 100644 index 000000000..5009da096 --- /dev/null +++ b/plugins/totp/src/frontend/TotpEnrollment.tsx @@ -0,0 +1,404 @@ +import { useEffect, useState } from "react"; +import { CheckCircle2, Copy, X } from "lucide-react"; +import { toast } from "sonner"; +import { Button, Input, copyToClipboard } from "@termix/plugin-sdk/ui"; +import { usePluginApi, useTranslation } from "@termix/plugin-sdk/frontend"; +import { apiErrorMessage } from "./totp-input"; + +type Step = "idle" | "setup" | "verify" | "backup"; + +interface SetupResponse { + secret: string; + qr_code: string; + additional?: boolean; +} + +const accentButton = + "text-xs border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand"; +const destructiveButton = + "text-xs border-destructive/40 text-destructive hover:bg-destructive/10 hover:text-destructive"; +const box = "border border-border bg-muted/20 p-3 flex flex-col gap-3"; +const boxTitle = + "text-[10px] font-semibold uppercase tracking-widest text-muted-foreground"; + +/** Settings > Security: turn TOTP on and off, add devices, backup codes. */ +export function TotpEnrollment() { + const { t } = useTranslation(); + const api = usePluginApi(); + const [enabled, setEnabled] = useState(false); + const [step, setStep] = useState("idle"); + const [qrCode, setQrCode] = useState(""); + const [secret, setSecret] = useState(""); + const [code, setCode] = useState(""); + const [backupCodes, setBackupCodes] = useState([]); + const [loading, setLoading] = useState(false); + const [showDisable, setShowDisable] = useState(false); + const [disableInput, setDisableInput] = useState(""); + const [showAdd, setShowAdd] = useState(false); + const [addInput, setAddInput] = useState(""); + const [adding, setAdding] = useState(false); + + useEffect(() => { + api + .get<{ enabled: boolean }>("status") + .then(({ data }) => setEnabled(!!data.enabled)) + .catch(() => {}); + }, [api]); + + async function startSetup() { + setLoading(true); + try { + const { data } = await api.post("setup", {}); + setQrCode(data.qr_code); + setSecret(data.secret); + setCode(""); + setStep("setup"); + } catch { + toast.error(t("enrollment.setupFailed")); + } finally { + setLoading(false); + } + } + + async function addAuthenticator() { + if (!addInput) { + toast.error(t("enrollment.codeRequired")); + return; + } + setLoading(true); + try { + const { data } = await api.post("setup", { + credential: addInput, + }); + setQrCode(data.qr_code); + setSecret(data.secret); + setAdding(true); + setShowAdd(false); + setAddInput(""); + setStep("setup"); + } catch (e) { + toast.error(apiErrorMessage(e, t("enrollment.addFailed"))); + } finally { + setLoading(false); + } + } + + async function verify() { + if (code.length !== 6) { + toast.error(t("enrollment.enter6Digits")); + return; + } + setLoading(true); + try { + const { data } = await api.post<{ backup_codes?: string[] }>("enable", { + totp_code: code, + }); + setBackupCodes(data.backup_codes ?? []); + setEnabled(true); + setStep("backup"); + toast.success(t("enrollment.enabledSuccess")); + } catch (e) { + toast.error(apiErrorMessage(e, t("enrollment.invalidCode"))); + } finally { + setLoading(false); + } + } + + async function disable() { + if (!disableInput) { + toast.error(t("enrollment.codeRequired")); + return; + } + setLoading(true); + try { + await api.post("disable", { totp_code: disableInput }); + setEnabled(false); + setShowDisable(false); + setDisableInput(""); + toast.success(t("enrollment.disabledSuccess")); + } catch (e) { + toast.error(apiErrorMessage(e, t("enrollment.disableFailed"))); + } finally { + setLoading(false); + } + } + + function downloadBackupCodes() { + const blob = new Blob([backupCodes.join("\n")], { type: "text/plain" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "termix-backup-codes.txt"; + a.click(); + URL.revokeObjectURL(url); + } + + return ( +
+
+
+
+ {t("enrollment.title")} + + {t("enrollment.docs")} + +
+ + {enabled ? t("enrollment.on") : t("enrollment.off")} + +
+ {enabled ? ( +
+ + +
+ ) : ( + + )} +
+ + {enabled && showAdd && ( +
+ {t("enrollment.addTitle")} + + {t("enrollment.addDescription")} + + setAddInput(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && addAuthenticator()} + className="text-sm" + /> +
+ + +
+
+ )} + + {enabled && showDisable && ( +
+ {t("enrollment.disableTitle")} + setDisableInput(e.target.value)} + className="text-sm" + /> +
+ + +
+
+ )} + + {step === "setup" && ( +
+
+ {t("enrollment.setupTitle")} + +
+
+ {qrCode ? ( + {t("enrollment.qrCode")} + ) : ( +
+ {t("enrollment.qrCode")} +
+ )} +
+
+ + {secret} + + +
+ + {t( + adding + ? "enrollment.addScanInstructions" + : "enrollment.instructions", + )} + + +
+ )} + + {!enabled && step === "verify" && ( +
+
+ {t("enrollment.verifyTitle")} + +
+ + setCode(e.target.value.replace(/\D/g, "").slice(0, 6)) + } + onKeyDown={(e) => e.key === "Enter" && verify()} + className="text-center font-mono tracking-widest text-lg h-10" + maxLength={6} + /> +
+ + +
+
+ )} + + {step === "backup" && backupCodes.length > 0 && ( +
+ {t("enrollment.backupTitle")} +
+ {backupCodes.map((backupCode) => ( + + {backupCode} + + ))} +
+ + +
+ )} +
+ ); +} diff --git a/plugins/totp/src/frontend/index.tsx b/plugins/totp/src/frontend/index.tsx new file mode 100644 index 000000000..e66e6a522 --- /dev/null +++ b/plugins/totp/src/frontend/index.tsx @@ -0,0 +1,12 @@ +import type { TermixApp } from "@termix/plugin-sdk/frontend"; +import { TotpChallenge } from "./TotpChallenge"; +import { TotpEnrollment } from "./TotpEnrollment"; + +export function activate(app: TermixApp): void { + app.registerSecondFactorUI({ + id: "totp", + titleKey: "title", + component: TotpChallenge, + enrollment: TotpEnrollment, + }); +} diff --git a/plugins/totp/src/frontend/totp-input.ts b/plugins/totp/src/frontend/totp-input.ts new file mode 100644 index 000000000..ed7dc53d5 --- /dev/null +++ b/plugins/totp/src/frontend/totp-input.ts @@ -0,0 +1,17 @@ +/** Six-digit codes, or a backup code of up to eight letters and digits. */ +export function normalizeTotpInput(value: string): string { + return value + .replace(/[^a-zA-Z0-9]/g, "") + .toUpperCase() + .slice(0, 8); +} + +export function isValidTotpInput(value: string): boolean { + return /^[A-Z0-9]{6,8}$/.test(value); +} + +export function apiErrorMessage(error: unknown, fallback: string): string { + const data = (error as { response?: { data?: { error?: unknown } } }) + ?.response?.data; + return typeof data?.error === "string" && data.error ? data.error : fallback; +} diff --git a/plugins/totp/tests/backend/helpers.ts b/plugins/totp/tests/backend/helpers.ts new file mode 100644 index 000000000..aae244b2a --- /dev/null +++ b/plugins/totp/tests/backend/helpers.ts @@ -0,0 +1,95 @@ +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 }, + // 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 }>; + close: () => Promise; +} + +/** + * The plugin activated against a real in-memory database, its router mounted + * on an express app the way core mounts it: JSON parsed, the acting user set + * from the request, everything else left to the plugin. + */ +export async function startServer( + options: { + users?: string[]; + capabilities?: string[]; + refuseEnrollment?: string; + } = {}, +): Promise { + const db = await createTestDb(pluginDir); + for (const user of options.users ?? ["user-1", "user-2"]) { + db.sqlite + .prepare("INSERT INTO users (id, username) VALUES (?, ?)") + .run(user, user); + } + + let router: Router | null = null; + const mock = createMockCtx({ + pluginId: manifest.id, + manifest, + capabilities: options.capabilities ?? manifest.capabilities, + db: db.database, + router: () => (router = express.Router()), + refuseEnrollment: options.refuseEnrollment, + }); + + const { activate } = await import("../../src/backend/index.js"); + await activate(mock.ctx); + + const app = express(); + app.use(express.json()); + 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 = "user-1", body: given } = {}) { + const body = method === "GET" ? undefined : given; + const response = await fetch(`http://127.0.0.1:${port}${path}`, { + method, + headers: { + "x-test-user": user, + ...(body !== undefined ? { "content-type": "application/json" } : {}), + }, + body: body !== undefined ? JSON.stringify(body) : undefined, + }); + const text = await response.text(); + return { status: response.status, body: text ? JSON.parse(text) : null }; + }, + async close() { + await new Promise((resolve) => server.close(() => resolve())); + db.close(); + }, + }; +} diff --git a/plugins/totp/tests/backend/totp.test.ts b/plugins/totp/tests/backend/totp.test.ts new file mode 100644 index 000000000..e0dc63464 --- /dev/null +++ b/plugins/totp/tests/backend/totp.test.ts @@ -0,0 +1,258 @@ +import { afterEach, describe, expect, it } from "vitest"; +import speakeasy from "speakeasy"; +import { startServer, type TestServer } from "./helpers"; +import { generateBackupCode, normalizeCode } from "../../src/backend/totp.js"; + +let server: TestServer | null = null; + +afterEach(async () => { + await server?.close(); + server = null; +}); + +const codeFor = (secret: string) => + speakeasy.totp({ secret, encoding: "base32" }); + +async function enrol(target: TestServer, user = "user-1") { + const setup = await target.request("POST", "/setup", { user, body: {} }); + expect(setup.status).toBe(200); + const secret: string = setup.body.secret; + const enable = await target.request("POST", "/enable", { + user, + body: { totp_code: codeFor(secret) }, + }); + expect(enable.status).toBe(200); + return { secret, backupCodes: enable.body.backup_codes as string[] }; +} + +function factor(target: TestServer) { + const registered = target.mock.auth.secondFactors.find( + (entry) => entry.id === "totp", + ); + if (!registered) throw new Error("totp factor not registered"); + return registered; +} + +describe("enrolment", () => { + it("sets up, enables, records the enrolment and reports status", async () => { + server = await startServer(); + expect((await server.request("GET", "/status")).body).toEqual({ + enabled: false, + }); + + const setup = await server.request("POST", "/setup", { body: {} }); + expect(setup.body.qr_code).toMatch(/^data:image\/png;base64,/); + expect(setup.body.secret).toMatch(/^[A-Z2-7]+$/); + + const wrong = await server.request("POST", "/enable", { + body: { totp_code: "000000" }, + }); + expect(wrong.status).toBe(401); + expect(await factor(server).isEnrolled("user-1")).toBe(false); + + const enable = await server.request("POST", "/enable", { + body: { totp_code: codeFor(setup.body.secret) }, + }); + expect(enable.status).toBe(200); + expect(enable.body.backup_codes).toHaveLength(8); + expect(server.mock.auth.enrollments.has("user-1:totp")).toBe(true); + expect((await server.request("GET", "/status")).body).toEqual({ + enabled: true, + }); + }); + + it("stores the secret sealed, never in the clear", async () => { + server = await startServer(); + const { secret } = await enrol(server); + const row = server.db.sqlite + .prepare("SELECT * FROM p_totp_enrollments WHERE user_id = 'user-1'") + .get() as Record; + expect(row.secret).not.toContain(secret); + expect(row.secret).toMatch(/^sealed:/); + expect(row.pending_secret).toBeNull(); + }); + + it("refuses enable before setup and when already on", async () => { + server = await startServer(); + expect( + ( + await server.request("POST", "/enable", { + body: { totp_code: "123456" }, + }) + ).status, + ).toBe(400); + const { secret } = await enrol(server); + expect( + ( + await server.request("POST", "/enable", { + body: { totp_code: codeFor(secret) }, + }) + ).status, + ).toBe(400); + }); + + it("leaves nothing enabled when core refuses the enrolment", async () => { + server = await startServer({ + refuseEnrollment: "Cannot enable 2FA while password login is disabled.", + }); + const setup = await server.request("POST", "/setup", { body: {} }); + const enable = await server.request("POST", "/enable", { + body: { totp_code: codeFor(setup.body.secret) }, + }); + expect(enable.status).toBe(409); + expect(enable.body.error).toMatch(/password login/); + expect(await factor(server).isEnrolled("user-1")).toBe(false); + }); + + it("adds another authenticator only with a valid code", async () => { + server = await startServer(); + const { secret } = await enrol(server); + expect((await server.request("POST", "/setup", { body: {} })).status).toBe( + 400, + ); + expect( + ( + await server.request("POST", "/setup", { + body: { credential: "000000" }, + }) + ).status, + ).toBe(401); + const again = await server.request("POST", "/setup", { + body: { credential: codeFor(secret) }, + }); + expect(again.body).toMatchObject({ secret, additional: true }); + }); + + it("disables with a code and removes the enrolment", async () => { + server = await startServer(); + const { secret } = await enrol(server); + expect( + ( + await server.request("POST", "/disable", { + body: { totp_code: "000000" }, + }) + ).status, + ).toBe(401); + const off = await server.request("POST", "/disable", { + body: { totp_code: codeFor(secret) }, + }); + expect(off.status).toBe(200); + expect(server.mock.auth.enrollments.has("user-1:totp")).toBe(false); + expect(await factor(server).isEnrolled("user-1")).toBe(false); + }); + + it("replaces backup codes", async () => { + server = await startServer(); + const { secret, backupCodes } = await enrol(server); + const fresh = await server.request("POST", "/backup-codes", { + body: { totp_code: codeFor(secret) }, + }); + expect(fresh.body.backup_codes).toHaveLength(8); + expect( + await factor(server).verify("user-1", { totp_code: backupCodes[0] }), + ).toBe(false); + expect( + await factor(server).verify("user-1", { + totp_code: fresh.body.backup_codes[0], + }), + ).toBe(true); + }); + + it("keeps users apart", async () => { + server = await startServer(); + await enrol(server, "user-1"); + expect( + (await server.request("GET", "/status", { user: "user-2" })).body, + ).toEqual({ enabled: false }); + }); +}); + +describe("login", () => { + it("verifies a current code and refuses a wrong one", async () => { + server = await startServer(); + const { secret } = await enrol(server); + expect(await factor(server).verify("user-1", { totp_code: "000000" })).toBe( + false, + ); + expect( + await factor(server).verify("user-1", { totp_code: codeFor(secret) }), + ).toBe(true); + }); + + it("accepts each backup code once, typed in any case", async () => { + server = await startServer(); + const { backupCodes } = await enrol(server); + const [first] = backupCodes; + expect( + await factor(server).verify("user-1", { + totp_code: first.toLowerCase(), + }), + ).toBe(true); + expect(await factor(server).verify("user-1", { totp_code: first })).toBe( + false, + ); + expect( + await factor(server).verify("user-1", { totp_code: backupCodes[1] }), + ).toBe(true); + }); + + it("refuses with a message when the user has no readable secret", async () => { + server = await startServer(); + expect( + await factor(server).verify("user-1", { totp_code: "123456" }), + ).toMatchObject({ ok: false }); + }); + + it("reset removes the enrolment, as the admin reset calls it", async () => { + server = await startServer(); + const { secret } = await enrol(server); + await factor(server).reset!("user-1"); + expect(await factor(server).isEnrolled("user-1")).toBe(false); + expect( + await factor(server).verify("user-1", { totp_code: codeFor(secret) }), + ).toMatchObject({ ok: false }); + }); + + it("drops the enrolment with the user", async () => { + server = await startServer(); + await enrol(server); + server.db.sqlite.exec("DELETE FROM users WHERE id = 'user-1'"); + expect(await factor(server).isEnrolled("user-1")).toBe(false); + }); +}); + +describe("capabilities", () => { + it("fails closed without auth:provide", async () => { + await expect( + startServer({ + capabilities: ["db:own", "secrets:own", "network:serve", "ui:surface"], + }), + ).rejects.toThrow(/auth:provide/); + }); + + it("fails closed without db:own", async () => { + await expect( + startServer({ + capabilities: [ + "secrets:own", + "network:serve", + "auth:provide", + "ui:surface", + ], + }), + ).rejects.toThrow(/db:own/); + }); +}); + +describe("helpers", () => { + it("makes backup codes from the unambiguous alphabet", () => { + for (let i = 0; i < 50; i++) { + expect(generateBackupCode()).toMatch(/^[A-HJ-NP-Z2-9]{8}$/); + } + }); + + it("normalises codes", () => { + expect(normalizeCode(" abcd1234 ")).toBe("ABCD1234"); + expect(normalizeCode(42)).toBe(""); + }); +}); diff --git a/plugins/totp/tests/frontend/activate.test.tsx b/plugins/totp/tests/frontend/activate.test.tsx new file mode 100644 index 000000000..ab2eb326b --- /dev/null +++ b/plugins/totp/tests/frontend/activate.test.tsx @@ -0,0 +1,90 @@ +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 { + isValidTotpInput, + normalizeTotpInput, +} from "../../src/frontend/totp-input"; +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 second factor with an enrolment section", async () => { + rendered = await renderWithApp(plugin, { manifest, locales }); + expect(rendered.registered.secondFactors()).toEqual(["totp"]); + }); + + it("sends the typed code, backup codes included", async () => { + rendered = await renderWithApp(plugin, { manifest, locales }); + const verify = vi.fn(async () => {}); + rendered.renderSecondFactor("totp", { verify }); + fireEvent.change(document.getElementById("totp-code")!, { + target: { value: "ab-cd 12 34" }, + }); + fireEvent.submit(document.getElementById("totp-code")!.closest("form")!); + await waitFor(() => + expect(verify).toHaveBeenCalledWith({ totp_code: "ABCD1234" }), + ); + }); + + it("shows the enrolment state from the plugin API", async () => { + rendered = await renderWithApp(plugin, { + manifest, + locales, + api: { + get: vi.fn(async () => ({ data: { enabled: true } })), + } as never, + }); + rendered.renderEnrollment("totp"); + expect(await screen.findByText(locales.enrollment.on)).toBeTruthy(); + expect(screen.getByText(locales.enrollment.addDevice)).toBeTruthy(); + }); + + it("starts setup and shows the QR code", async () => { + const post = vi.fn(async () => ({ + data: { secret: "JBSWY3DPEHPK3PXP", qr_code: "data:image/png;base64,x" }, + })); + rendered = await renderWithApp(plugin, { + manifest, + locales, + api: { + get: vi.fn(async () => ({ data: { enabled: false } })), + post, + } as never, + }); + rendered.renderEnrollment("totp"); + fireEvent.click(await screen.findByText(locales.enrollment.enable)); + expect(await screen.findByText("JBSWY3DPEHPK3PXP")).toBeTruthy(); + expect(post).toHaveBeenCalledWith("setup", {}); + }); + + it("removes the factor on deactivate", async () => { + const app = await renderWithApp(plugin, { manifest, locales }); + await app.deactivate(); + expect(app.registered.secondFactors()).toEqual([]); + }); +}); + +describe("totp input", () => { + it("normalises and validates codes", () => { + expect(normalizeTotpInput("12 34-56")).toBe("123456"); + expect(normalizeTotpInput("abcd1234ef")).toBe("ABCD1234"); + expect(isValidTotpInput("123456")).toBe(true); + expect(isValidTotpInput("ABCD1234")).toBe(true); + expect(isValidTotpInput("12345")).toBe(false); + }); +}); diff --git a/plugins/totp/tsconfig.json b/plugins/totp/tsconfig.json new file mode 100644 index 000000000..7b35314c3 --- /dev/null +++ b/plugins/totp/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/totp/vitest.config.ts b/plugins/totp/vitest.config.ts new file mode 100644 index 000000000..31ba004fc --- /dev/null +++ b/plugins/totp/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/webauthn/locales/en.json b/plugins/webauthn/locales/en.json new file mode 100644 index 000000000..18248e606 --- /dev/null +++ b/plugins/webauthn/locales/en.json @@ -0,0 +1,22 @@ +{ + "title": "Passkeys", + "signIn": "Sign in with passkey", + "loginFailed": "Passkey login failed", + "description": "Use WebAuthn/FIDO2 credentials to sign in without a password", + "namePlaceholder": "Passkey name", + "defaultName": "Passkey", + "uv": { + "preferred": "Preferred", + "required": "Required", + "discouraged": "Discouraged" + }, + "add": "Add Passkey", + "none": "No passkeys registered.", + "added": "Passkey added", + "addFailed": "Failed to add passkey", + "delete": "Delete passkey", + "deleted": "Passkey deleted", + "deleteFailed": "Failed to delete passkey", + "unknownDevice": "unknown", + "synced": "synced" +} diff --git a/plugins/webauthn/locales/translated/af_ZA.json b/plugins/webauthn/locales/translated/af_ZA.json new file mode 100644 index 000000000..d35fa1ac0 --- /dev/null +++ b/plugins/webauthn/locales/translated/af_ZA.json @@ -0,0 +1,18 @@ +{ + "title": "Wagwoorde", + "signIn": "Teken aan met wagwoord", + "loginFailed": "Wagwoord-aanmelding het misluk", + "description": "Gebruik WebAuthn/FIDO2-bewyse om aan te meld sonder 'n wagwoord", + "namePlaceholder": "Wagwoordnaam", + "uv": { + "preferred": "Verkies", + "required": "Vereis", + "discouraged": "Ontmoedig" + }, + "add": "Voeg wagwoord by", + "none": "Geen wagwoorde geregistreer nie.", + "added": "Wagwoord bygevoeg", + "addFailed": "Kon nie wagwoord byvoeg nie", + "deleted": "Wagwoord verwyder", + "deleteFailed": "Kon nie wagwoord verwyder nie" +} diff --git a/plugins/webauthn/locales/translated/ar_SA.json b/plugins/webauthn/locales/translated/ar_SA.json new file mode 100644 index 000000000..8adeb5fff --- /dev/null +++ b/plugins/webauthn/locales/translated/ar_SA.json @@ -0,0 +1,18 @@ +{ + "title": "مفاتيح المرور", + "signIn": "تسجيل الدخول باستخدام مفتاح المرور", + "loginFailed": "فشل تسجيل الدخول بمفتاح المرور", + "description": "استخدم بيانات اعتماد WebAuthn/FIDO2 لتسجيل الدخول بدون كلمة مرور", + "namePlaceholder": "اسم مفتاح المرور", + "uv": { + "preferred": "مفضل", + "required": "مطلوب", + "discouraged": "غير مشجع" + }, + "add": "إضافة مفتاح مرور", + "none": "لا توجد مفاتيح مرور مسجلة.", + "added": "تمت إضافة مفتاح المرور", + "addFailed": "فشل إضافة مفتاح المرور", + "deleted": "تم حذف مفتاح المرور", + "deleteFailed": "فشل حذف مفتاح المرور" +} diff --git a/plugins/webauthn/locales/translated/bg_BG.json b/plugins/webauthn/locales/translated/bg_BG.json new file mode 100644 index 000000000..fea409b06 --- /dev/null +++ b/plugins/webauthn/locales/translated/bg_BG.json @@ -0,0 +1,18 @@ +{ + "title": "Пароли", + "signIn": "Влезте с парола", + "loginFailed": "Влизането с парола не бе успешно", + "description": "Използвайте WebAuthn/FIDO2 идентификационни данни за влизане без парола", + "namePlaceholder": "Име на паролата", + "uv": { + "preferred": "Предпочитан", + "required": "Задължително", + "discouraged": "Обезкуражен" + }, + "add": "Добавяне на парола", + "none": "Няма регистрирани пароли.", + "added": "Добавен е ключ за достъп", + "addFailed": "Добавянето на ключ за достъп не бе успешно", + "deleted": "Ключът за достъп е изтрит", + "deleteFailed": "Изтриването на ключа за достъп не бе успешно" +} diff --git a/plugins/webauthn/locales/translated/bn_BD.json b/plugins/webauthn/locales/translated/bn_BD.json new file mode 100644 index 000000000..116837454 --- /dev/null +++ b/plugins/webauthn/locales/translated/bn_BD.json @@ -0,0 +1,18 @@ +{ + "title": "পাসকি", + "signIn": "পাসকি দিয়ে সাইন ইন করুন", + "loginFailed": "পাসকি লগইন ব্যর্থ হয়েছে", + "description": "পাসওয়ার্ড ছাড়া সাইন ইন করতে WebAuthn/FIDO2 ক্রেডেনশিয়াল ব্যবহার করুন।", + "namePlaceholder": "পাসকি নাম", + "uv": { + "preferred": "পছন্দের", + "required": "প্রয়োজনীয়", + "discouraged": "হতাশ" + }, + "add": "পাসকি যোগ করুন", + "none": "কোনো পাসকি নিবন্ধিত নেই।", + "added": "পাসকি যোগ করা হয়েছে", + "addFailed": "পাসকি যোগ করতে ব্যর্থ হয়েছে", + "deleted": "পাসকি মুছে ফেলা হয়েছে", + "deleteFailed": "পাসকি মুছে ফেলতে ব্যর্থ হয়েছে" +} diff --git a/plugins/webauthn/locales/translated/ca_ES.json b/plugins/webauthn/locales/translated/ca_ES.json new file mode 100644 index 000000000..c3967fc6d --- /dev/null +++ b/plugins/webauthn/locales/translated/ca_ES.json @@ -0,0 +1,18 @@ +{ + "title": "Claus d'accés", + "signIn": "Inicia la sessió amb la clau d'accés", + "loginFailed": "Error d'inici de sessió amb contrasenya", + "description": "Utilitzeu les credencials de WebAuthn/FIDO2 per iniciar la sessió sense contrasenya", + "namePlaceholder": "Nom de la clau d'accés", + "uv": { + "preferred": "Preferit", + "required": "Obligatori", + "discouraged": "Desanimat" + }, + "add": "Afegeix una contrasenya", + "none": "No hi ha cap clau d'accés registrada.", + "added": "Clau d'accés afegida", + "addFailed": "No s'ha pogut afegir la contrasenya", + "deleted": "Clau d'accés suprimida", + "deleteFailed": "No s'ha pogut suprimir la clau d'accés" +} diff --git a/plugins/webauthn/locales/translated/cs_CZ.json b/plugins/webauthn/locales/translated/cs_CZ.json new file mode 100644 index 000000000..2c09b18b6 --- /dev/null +++ b/plugins/webauthn/locales/translated/cs_CZ.json @@ -0,0 +1,18 @@ +{ + "title": "Přístupové klíče", + "signIn": "Přihlásit se pomocí hesla", + "loginFailed": "Přihlášení k heslu se nezdařilo", + "description": "Pro přihlášení bez hesla použijte přihlašovací údaje WebAuthn/FIDO2", + "namePlaceholder": "Název přístupového klíče", + "uv": { + "preferred": "Preferované", + "required": "Požadovaný", + "discouraged": "Odradit" + }, + "add": "Přidat přístupový klíč", + "none": "Nejsou registrovány žádné přístupové klíče.", + "added": "Přístupový klíč byl přidán", + "addFailed": "Nepodařilo se přidat přístupový klíč", + "deleted": "Přístupový klíč smazán", + "deleteFailed": "Nepodařilo se smazat přístupový klíč" +} diff --git a/plugins/webauthn/locales/translated/da_DK.json b/plugins/webauthn/locales/translated/da_DK.json new file mode 100644 index 000000000..94e6a3eaa --- /dev/null +++ b/plugins/webauthn/locales/translated/da_DK.json @@ -0,0 +1,18 @@ +{ + "title": "Adgangsnøgler", + "signIn": "Log ind med adgangsnøgle", + "loginFailed": "Login med adgangsnøgle mislykkedes", + "description": "Brug WebAuthn/FIDO2-legitimationsoplysninger til at logge ind uden en adgangskode", + "namePlaceholder": "Adgangsnøglenavn", + "uv": { + "preferred": "Foretrukket", + "required": "Påkrævet", + "discouraged": "Modløs" + }, + "add": "Tilføj adgangsnøgle", + "none": "Ingen adgangsnøgler registreret.", + "added": "Adgangsnøgle tilføjet", + "addFailed": "Kunne ikke tilføje adgangsnøgle", + "deleted": "Adgangsnøgle slettet", + "deleteFailed": "Kunne ikke slette adgangsnøglen" +} diff --git a/plugins/webauthn/locales/translated/de_DE.json b/plugins/webauthn/locales/translated/de_DE.json new file mode 100644 index 000000000..c24857278 --- /dev/null +++ b/plugins/webauthn/locales/translated/de_DE.json @@ -0,0 +1,18 @@ +{ + "title": "Passkeys", + "signIn": "Mit Passkey anmelden", + "loginFailed": "Passkey-Anmeldung fehlgeschlagen", + "description": "WebAuthn/FIDO2-Anmeldedaten für eine Anmeldung ohne Passwort verwenden", + "namePlaceholder": "Passkey-Name", + "uv": { + "preferred": "Bevorzugt", + "required": "Erforderlich", + "discouraged": "Nicht empfohlen" + }, + "add": "Passkey hinzufügen", + "none": "Keine Passkeys registriert.", + "added": "Passkey hinzugefügt", + "addFailed": "Fehler beim Hinzufügen des Passkeys", + "deleted": "Passkey gelöscht", + "deleteFailed": "Löschen des Passkeys fehlgeschlagen" +} diff --git a/plugins/webauthn/locales/translated/el_GR.json b/plugins/webauthn/locales/translated/el_GR.json new file mode 100644 index 000000000..300fbfe1e --- /dev/null +++ b/plugins/webauthn/locales/translated/el_GR.json @@ -0,0 +1,18 @@ +{ + "title": "Κλειδιά πρόσβασης", + "signIn": "Σύνδεση με κλειδί πρόσβασης", + "loginFailed": "Η σύνδεση με κωδικό πρόσβασης απέτυχε", + "description": "Χρησιμοποιήστε διαπιστευτήρια WebAuthn/FIDO2 για να συνδεθείτε χωρίς κωδικό πρόσβασης", + "namePlaceholder": "Όνομα κλειδιού πρόσβασης", + "uv": { + "preferred": "Προνομιούχος", + "required": "Υποχρεούμαι", + "discouraged": "Αποθαρρυμένος" + }, + "add": "Προσθήκη κλειδιού πρόσβασης", + "none": "Δεν έχουν καταχωρηθεί κλειδιά πρόσβασης.", + "added": "Προστέθηκε κλειδί πρόσβασης", + "addFailed": "Αποτυχία προσθήκης κλειδιού πρόσβασης", + "deleted": "Ο κωδικός πρόσβασης διαγράφηκε", + "deleteFailed": "Η διαγραφή του κλειδιού πρόσβασης απέτυχε" +} diff --git a/plugins/webauthn/locales/translated/es_ES.json b/plugins/webauthn/locales/translated/es_ES.json new file mode 100644 index 000000000..956e48368 --- /dev/null +++ b/plugins/webauthn/locales/translated/es_ES.json @@ -0,0 +1,18 @@ +{ + "title": "Passkeys", + "signIn": "Iniciar sesión con clave de acceso", + "loginFailed": "Error al iniciar sesión con clave de acceso", + "description": "Usa credenciales WebAuthn/FIDO2 para iniciar sesión sin contraseña", + "namePlaceholder": "Nombre de la passkey", + "uv": { + "preferred": "Preferida", + "required": "Requerida", + "discouraged": "Desaconsejada" + }, + "add": "Añadir passkey", + "none": "No hay passkeys registradas.", + "added": "Passkey añadida", + "addFailed": "No se pudo añadir la passkey", + "deleted": "Passkey eliminada", + "deleteFailed": "Error al eliminar la clave de acceso" +} diff --git a/plugins/webauthn/locales/translated/fi_FI.json b/plugins/webauthn/locales/translated/fi_FI.json new file mode 100644 index 000000000..693d85214 --- /dev/null +++ b/plugins/webauthn/locales/translated/fi_FI.json @@ -0,0 +1,18 @@ +{ + "title": "Salasanat", + "signIn": "Kirjaudu sisään salasanalla", + "loginFailed": "Salasanan kirjautuminen epäonnistui", + "description": "Kirjaudu sisään ilman salasanaa WebAuthn/FIDO2-tunnuksilla", + "namePlaceholder": "Salasanan nimi", + "uv": { + "preferred": "Ensisijainen", + "required": "Pakollinen", + "discouraged": "Lannistunut" + }, + "add": "Lisää salasana", + "none": "Ei rekisteröityjä salasanoja.", + "added": "Salasana lisätty", + "addFailed": "Salasanan lisääminen epäonnistui", + "deleted": "Salasana poistettu", + "deleteFailed": "Salasanan poistaminen epäonnistui" +} diff --git a/plugins/webauthn/locales/translated/fr_FR.json b/plugins/webauthn/locales/translated/fr_FR.json new file mode 100644 index 000000000..6c8993ae6 --- /dev/null +++ b/plugins/webauthn/locales/translated/fr_FR.json @@ -0,0 +1,18 @@ +{ + "title": "Clés d’accès", + "signIn": "Se connecter avec une clé d'accès", + "loginFailed": "Échec de la connexion par clé d'accès", + "description": "Utilisez des identifiants WebAuthn/FIDO2 pour vous connecter sans mot de passe", + "namePlaceholder": "Nom de la clé d’accès", + "uv": { + "preferred": "Préféré", + "required": "Requis", + "discouraged": "Déconseillé" + }, + "add": "Ajouter une clé d’accès", + "none": "Aucune clé d’accès enregistrée.", + "added": "Clé d’accès ajoutée", + "addFailed": "Échec de l’ajout de la clé d’accès", + "deleted": "Clé d’accès supprimée", + "deleteFailed": "Échec de la suppression de la clé d'accès" +} diff --git a/plugins/webauthn/locales/translated/he_IL.json b/plugins/webauthn/locales/translated/he_IL.json new file mode 100644 index 000000000..1c8105d27 --- /dev/null +++ b/plugins/webauthn/locales/translated/he_IL.json @@ -0,0 +1,18 @@ +{ + "title": "מפתחות סיסמה", + "signIn": "כניסה עם סיסמה", + "loginFailed": "כניסה עם סיסמה נכשלה", + "description": "השתמש בפרטי הכניסה של WebAuthn/FIDO2 ללא סיסמה", + "namePlaceholder": "שם המפתח", + "uv": { + "preferred": "מועדף", + "required": "דָרוּשׁ", + "discouraged": "מיואש" + }, + "add": "הוסף סיסמה", + "none": "אין סיסמות רשומות.", + "added": "סיסמה נוספת", + "addFailed": "נכשלה הוספת סיסמה", + "deleted": "סיסמת הגישה נמחקה", + "deleteFailed": "מחיקת המפתח נכשלה" +} diff --git a/plugins/webauthn/locales/translated/hi_IN.json b/plugins/webauthn/locales/translated/hi_IN.json new file mode 100644 index 000000000..3f1dfee6d --- /dev/null +++ b/plugins/webauthn/locales/translated/hi_IN.json @@ -0,0 +1,18 @@ +{ + "title": "पासकीज़", + "signIn": "पासकी से साइन इन करें", + "loginFailed": "पासकी लॉगिन विफल", + "description": "बिना पासवर्ड साइन इन करने के लिए WebAuthn/FIDO2 क्रेडेंशियल का उपयोग करें", + "namePlaceholder": "पासकी नाम", + "uv": { + "preferred": "पसंदीदा", + "required": "आवश्यक", + "discouraged": "हतोत्साहित" + }, + "add": "पासकी जोड़ें", + "none": "कोई पासकी पंजीकृत नहीं है।", + "added": "पासकी जोड़ी गई", + "addFailed": "पासकी जोड़ने में विफल", + "deleted": "पासकी हटाई गई", + "deleteFailed": "पासकी हटाने में विफल" +} diff --git a/plugins/webauthn/locales/translated/hu_HU.json b/plugins/webauthn/locales/translated/hu_HU.json new file mode 100644 index 000000000..a5c52a2be --- /dev/null +++ b/plugins/webauthn/locales/translated/hu_HU.json @@ -0,0 +1,18 @@ +{ + "title": "Jelszavak", + "signIn": "Bejelentkezés jelszóval", + "loginFailed": "Jelszóval történő bejelentkezés sikertelen", + "description": "Jelszó nélküli bejelentkezéshez használja a WebAuthn/FIDO2 hitelesítő adatokat", + "namePlaceholder": "Jelszó neve", + "uv": { + "preferred": "Előnyben részesített", + "required": "Kívánt", + "discouraged": "Elbátortalanodva" + }, + "add": "Jelszó hozzáadása", + "none": "Nincsenek regisztrált jelszók.", + "added": "Hozzáadott jelszó", + "addFailed": "Nem sikerült hozzáadni a jelszót", + "deleted": "Jelszó törölve", + "deleteFailed": "Nem sikerült törölni a jelszót" +} diff --git a/plugins/webauthn/locales/translated/id_ID.json b/plugins/webauthn/locales/translated/id_ID.json new file mode 100644 index 000000000..732127abe --- /dev/null +++ b/plugins/webauthn/locales/translated/id_ID.json @@ -0,0 +1,18 @@ +{ + "title": "Kode akses", + "signIn": "Masuk dengan kata sandi", + "loginFailed": "Login dengan kata sandi gagal.", + "description": "Gunakan kredensial WebAuthn/FIDO2 untuk masuk tanpa kata sandi.", + "namePlaceholder": "Nama kata sandi", + "uv": { + "preferred": "Lebih disukai", + "required": "Diperlukan", + "discouraged": "Berkecil hati" + }, + "add": "Tambahkan Kata Sandi", + "none": "Tidak ada kata sandi yang terdaftar.", + "added": "Kata sandi telah ditambahkan", + "addFailed": "Gagal menambahkan kata sandi.", + "deleted": "Kata sandi telah dihapus.", + "deleteFailed": "Gagal menghapus kata sandi." +} diff --git a/plugins/webauthn/locales/translated/it_IT.json b/plugins/webauthn/locales/translated/it_IT.json new file mode 100644 index 000000000..b422186f4 --- /dev/null +++ b/plugins/webauthn/locales/translated/it_IT.json @@ -0,0 +1,18 @@ +{ + "title": "Passkeys", + "signIn": "Accedi con passkey", + "loginFailed": "Accesso con passkey non riuscito", + "description": "Utilizza credenziali WebAuthn/FIDO2 per accedere senza password", + "namePlaceholder": "Nome passkey", + "uv": { + "preferred": "Preferita", + "required": "Obbligatoria", + "discouraged": "Sconsigliata" + }, + "add": "Aggiungi passkey", + "none": "Nessuna passkey registrata.", + "added": "Passkey aggiunta", + "addFailed": "Impossibile aggiungere la passkey", + "deleted": "Passkey eliminata", + "deleteFailed": "Impossibile eliminare la passkey" +} diff --git a/plugins/webauthn/locales/translated/ja_JP.json b/plugins/webauthn/locales/translated/ja_JP.json new file mode 100644 index 000000000..0ba50fdc2 --- /dev/null +++ b/plugins/webauthn/locales/translated/ja_JP.json @@ -0,0 +1,18 @@ +{ + "title": "パスキー", + "signIn": "パスキーでサインイン", + "loginFailed": "パスキーログインに失敗しました", + "description": "WebAuthn/FIDO2認証情報を使用してパスワードなしでサインインできます", + "namePlaceholder": "パスキー名", + "uv": { + "preferred": "優先", + "required": "必須", + "discouraged": "非推奨" + }, + "add": "パスキーを追加", + "none": "登録されたパスキーはありません。", + "added": "パスキーが追加されました", + "addFailed": "パスキーの追加に失敗しました", + "deleted": "パスキーが削除されました", + "deleteFailed": "パスキーの削除に失敗しました" +} diff --git a/plugins/webauthn/locales/translated/ko_KR.json b/plugins/webauthn/locales/translated/ko_KR.json new file mode 100644 index 000000000..8075ab50d --- /dev/null +++ b/plugins/webauthn/locales/translated/ko_KR.json @@ -0,0 +1,18 @@ +{ + "title": "패스키", + "signIn": "Passkey로 로그인", + "loginFailed": "패스키 로그인 실패", + "description": "WebAuthn/FIDO2 자격 증명을 사용하여 비밀번호 없이 로그인하세요.", + "namePlaceholder": "패스키 이름", + "uv": { + "preferred": "선호", + "required": "필수", + "discouraged": "권장 안 함" + }, + "add": "패스키 추가", + "none": "등록된 패스키가 없습니다.", + "added": "패스키가 추가되었습니다.", + "addFailed": "패스키 추가 실패", + "deleted": "패스키가 삭제되었습니다.", + "deleteFailed": "패스키 삭제 실패" +} diff --git a/plugins/webauthn/locales/translated/nl_NL.json b/plugins/webauthn/locales/translated/nl_NL.json new file mode 100644 index 000000000..9ff7f64aa --- /dev/null +++ b/plugins/webauthn/locales/translated/nl_NL.json @@ -0,0 +1,18 @@ +{ + "title": "Wachtwoorden", + "signIn": "Aanmelden met wachtwoord", + "loginFailed": "Aanmelden met wachtwoord mislukt", + "description": "Gebruik WebAuthn/FIDO2-gegevens om zonder wachtwoord in te loggen.", + "namePlaceholder": "Wachtwoordnaam", + "uv": { + "preferred": "Voorkeur", + "required": "Vereist", + "discouraged": "Ontmoedigd" + }, + "add": "Voeg wachtwoord toe", + "none": "Er zijn geen wachtwoorden geregistreerd.", + "added": "Wachtwoord toegevoegd", + "addFailed": "Wachtwoord toevoegen mislukt", + "deleted": "Wachtwoord verwijderd", + "deleteFailed": "Het verwijderen van de toegangscode is mislukt." +} diff --git a/plugins/webauthn/locales/translated/no_NO.json b/plugins/webauthn/locales/translated/no_NO.json new file mode 100644 index 000000000..789ff02fe --- /dev/null +++ b/plugins/webauthn/locales/translated/no_NO.json @@ -0,0 +1,18 @@ +{ + "title": "Passord", + "signIn": "Logg inn med passord", + "loginFailed": "Innlogging med passord mislyktes", + "description": "Bruk WebAuthn/FIDO2-legitimasjon for å logge på uten passord", + "namePlaceholder": "Navn på passord", + "uv": { + "preferred": "Foretrukket", + "required": "Obligatorisk", + "discouraged": "Motløs" + }, + "add": "Legg til tilgangsnøkkel", + "none": "Ingen passord registrert.", + "added": "Tilgangsnøkkel lagt til", + "addFailed": "Kunne ikke legge til passord", + "deleted": "Tilgangsnøkkel slettet", + "deleteFailed": "Kunne ikke slette passordet" +} diff --git a/plugins/webauthn/locales/translated/pl_PL.json b/plugins/webauthn/locales/translated/pl_PL.json new file mode 100644 index 000000000..64b2dc15c --- /dev/null +++ b/plugins/webauthn/locales/translated/pl_PL.json @@ -0,0 +1,18 @@ +{ + "title": "Klucze dostępu", + "signIn": "Zaloguj się za pomocą klucza dostępu", + "loginFailed": "Nie udało się zalogować za pomocą klucza dostępu", + "description": "Używaj poświadczeń WebAuthn/FIDO2, aby logować się bez hasła", + "namePlaceholder": "Nazwa klucza dostępu", + "uv": { + "preferred": "Preferowane", + "required": "Wymagane", + "discouraged": "Odradzane" + }, + "add": "Dodaj klucz dostępu", + "none": "Nie zarejestrowano żadnych kluczy dostępu.", + "added": "Dodano klucz dostępu", + "addFailed": "Nie udało się dodać klucza dostępu", + "deleted": "Usunięto klucz dostępu", + "deleteFailed": "Nie udało się usunąć klucza dostępu" +} diff --git a/plugins/webauthn/locales/translated/pt_BR.json b/plugins/webauthn/locales/translated/pt_BR.json new file mode 100644 index 000000000..9132c2a21 --- /dev/null +++ b/plugins/webauthn/locales/translated/pt_BR.json @@ -0,0 +1,18 @@ +{ + "title": "Senhas", + "signIn": "Faça login com a chave de acesso.", + "loginFailed": "Falha no login com a chave de acesso", + "description": "Use as credenciais WebAuthn/FIDO2 para fazer login sem senha.", + "namePlaceholder": "Nome da chave de acesso", + "uv": { + "preferred": "Preferido", + "required": "Obrigatório", + "discouraged": "Desencorajado" + }, + "add": "Adicionar chave de acesso", + "none": "Nenhuma chave de acesso registrada.", + "added": "Senha adicionada", + "addFailed": "Falha ao adicionar a chave de acesso.", + "deleted": "Senha excluída", + "deleteFailed": "Falha ao excluir a chave de acesso." +} diff --git a/plugins/webauthn/locales/translated/pt_PT.json b/plugins/webauthn/locales/translated/pt_PT.json new file mode 100644 index 000000000..aa52a6075 --- /dev/null +++ b/plugins/webauthn/locales/translated/pt_PT.json @@ -0,0 +1,18 @@ +{ + "title": "Passkeys", + "signIn": "Iniciar sessão com passkey", + "loginFailed": "Falha no início de sessão com passkey", + "description": "Utilize credenciais WebAuthn/FIDO2 para iniciar sessão sem palavra-passe", + "namePlaceholder": "Nome da passkey", + "uv": { + "preferred": "Preferencial", + "required": "Obrigatório", + "discouraged": "Desaconselhado" + }, + "add": "Adicionar Passkey", + "none": "Nenhuma passkey registada.", + "added": "Passkey adicionada", + "addFailed": "Falha ao adicionar passkey", + "deleted": "Passkey eliminada", + "deleteFailed": "Falha ao eliminar chave de acesso" +} diff --git a/plugins/webauthn/locales/translated/ro_RO.json b/plugins/webauthn/locales/translated/ro_RO.json new file mode 100644 index 000000000..279f41542 --- /dev/null +++ b/plugins/webauthn/locales/translated/ro_RO.json @@ -0,0 +1,18 @@ +{ + "title": "Chei de acces", + "signIn": "Conectați-vă cu cheia de acces", + "loginFailed": "Conectarea cu parola a eșuat", + "description": "Folosește acreditările WebAuthn/FIDO2 pentru a te conecta fără parolă", + "namePlaceholder": "Numele cheii de acces", + "uv": { + "preferred": "Preferat", + "required": "Necesar", + "discouraged": "Descurajat" + }, + "add": "Adăugați o cheie de acces", + "none": "Nicio cheie de acces înregistrată.", + "added": "Cheie de acces adăugată", + "addFailed": "Nu s-a putut adăuga parola", + "deleted": "Cheia de acces a fost ștearsă", + "deleteFailed": "Nu s-a putut șterge parola" +} diff --git a/plugins/webauthn/locales/translated/ru_RU.json b/plugins/webauthn/locales/translated/ru_RU.json new file mode 100644 index 000000000..1be8b03b0 --- /dev/null +++ b/plugins/webauthn/locales/translated/ru_RU.json @@ -0,0 +1,18 @@ +{ + "title": "Ключи доступа", + "signIn": "Войти с ключом доступа", + "loginFailed": "Вход по ключу не удался", + "description": "Используйте учётные данные WebAuthn/FIDO2 для входа без пароля", + "namePlaceholder": "Название ключа доступа", + "uv": { + "preferred": "Предпочтительно", + "required": "Обязательно", + "discouraged": "Не рекомендуется" + }, + "add": "Добавить ключ доступа", + "none": "Нет зарегистрированных ключей доступа.", + "added": "Ключ доступа добавлен", + "addFailed": "Не удалось добавить ключ доступа", + "deleted": "Ключ доступа удалён", + "deleteFailed": "Ошибка удаления ключа доступа" +} diff --git a/plugins/webauthn/locales/translated/sr_SP.json b/plugins/webauthn/locales/translated/sr_SP.json new file mode 100644 index 000000000..37c470cb0 --- /dev/null +++ b/plugins/webauthn/locales/translated/sr_SP.json @@ -0,0 +1,18 @@ +{ + "title": "Лозинке", + "signIn": "Пријавите се помоћу лозинке", + "loginFailed": "Пријава лозинком није успела", + "description": "Користите WebAuthn/FIDO2 акредитиве за пријаву без лозинке", + "namePlaceholder": "Назив лозинке", + "uv": { + "preferred": "Преферирано", + "required": "Обавезно", + "discouraged": "Обесхрабрено" + }, + "add": "Додај лозинку", + "none": "Није регистрована ниједна лозинка.", + "added": "Лозинка је додата", + "addFailed": "Додавање лозинке није успело", + "deleted": "Лозинка је обрисана", + "deleteFailed": "Брисање лозинке није успело" +} diff --git a/plugins/webauthn/locales/translated/sv_SE.json b/plugins/webauthn/locales/translated/sv_SE.json new file mode 100644 index 000000000..36bc9f92a --- /dev/null +++ b/plugins/webauthn/locales/translated/sv_SE.json @@ -0,0 +1,18 @@ +{ + "title": "Lösenord", + "signIn": "Logga in med lösenord", + "loginFailed": "Inloggning med lösenordsnyckel misslyckades", + "description": "Använd WebAuthn/FIDO2-inloggningsuppgifter för att logga in utan lösenord", + "namePlaceholder": "Lösenordsnamn", + "uv": { + "preferred": "Föredragen", + "required": "Nödvändig", + "discouraged": "Avskräckt" + }, + "add": "Lägg till lösenord", + "none": "Inga lösennycklar registrerade.", + "added": "Lösenord tillagt", + "addFailed": "Misslyckades med att lägga till lösenord", + "deleted": "Lösenordet raderat", + "deleteFailed": "Misslyckades med att ta bort lösenordet" +} diff --git a/plugins/webauthn/locales/translated/th_TH.json b/plugins/webauthn/locales/translated/th_TH.json new file mode 100644 index 000000000..70a026ff1 --- /dev/null +++ b/plugins/webauthn/locales/translated/th_TH.json @@ -0,0 +1,18 @@ +{ + "title": "รหัสผ่าน", + "signIn": "ลงชื่อเข้าใช้ด้วยรหัสผ่าน", + "loginFailed": "การเข้าสู่ระบบด้วยรหัสผ่านล้มเหลว", + "description": "ใช้ข้อมูลรับรอง WebAuthn/FIDO2 เพื่อเข้าสู่ระบบโดยไม่ต้องใช้รหัสผ่าน", + "namePlaceholder": "ชื่อรหัสผ่าน", + "uv": { + "preferred": "ที่ต้องการ", + "required": "ที่จำเป็น", + "discouraged": "ท้อใจ" + }, + "add": "เพิ่มรหัสผ่าน", + "none": "ไม่มีการลงทะเบียนรหัสผ่านใดๆ", + "added": "เพิ่มรหัสผ่านแล้ว", + "addFailed": "ไม่สามารถเพิ่มรหัสผ่านได้", + "deleted": "รหัสผ่านถูกลบแล้ว", + "deleteFailed": "ไม่สามารถลบรหัสผ่านได้" +} diff --git a/plugins/webauthn/locales/translated/tr_TR.json b/plugins/webauthn/locales/translated/tr_TR.json new file mode 100644 index 000000000..ff4f894a0 --- /dev/null +++ b/plugins/webauthn/locales/translated/tr_TR.json @@ -0,0 +1,18 @@ +{ + "title": "Geçiş Anahtarları", + "signIn": "passkey ile oturum aç", + "loginFailed": "Passkey girişi başarısız oldu", + "description": "Parola olmadan oturum açmak için WebAuthn/FIDO2 kimlik bilgilerini kullanın", + "namePlaceholder": "Geçiş anahtarı adı", + "uv": { + "preferred": "Tercih edilen", + "required": "Gerekli", + "discouraged": "Önerilmeyen" + }, + "add": "Geçiş Anahtarı Ekle", + "none": "Kayıtlı geçiş anahtarı yok.", + "added": "Geçiş anahtarı eklendi", + "addFailed": "Geçiş anahtarı eklenemedi", + "deleted": "Geçiş anahtarı silindi", + "deleteFailed": "Geçiş anahtarı silinemedi" +} diff --git a/plugins/webauthn/locales/translated/uk_UA.json b/plugins/webauthn/locales/translated/uk_UA.json new file mode 100644 index 000000000..e04dba05f --- /dev/null +++ b/plugins/webauthn/locales/translated/uk_UA.json @@ -0,0 +1,18 @@ +{ + "title": "Ключі доступу", + "signIn": "Увійти за допомогою ключа доступу", + "loginFailed": "Не вдалося ввійти з ключем доступу", + "description": "Використовуйте облікові дані WebAuthn/FIDO2 для входу без пароля", + "namePlaceholder": "Ім'я ключа доступу", + "uv": { + "preferred": "Бажаний", + "required": "Обов'язково", + "discouraged": "Знеохочений" + }, + "add": "Додати пароль", + "none": "Не зареєстровано ключів доступу.", + "added": "Ключ доступу додано", + "addFailed": "Не вдалося додати ключ доступу", + "deleted": "Ключ доступу видалено", + "deleteFailed": "Не вдалося видалити ключ доступу" +} diff --git a/plugins/webauthn/locales/translated/vi_VN.json b/plugins/webauthn/locales/translated/vi_VN.json new file mode 100644 index 000000000..226c91191 --- /dev/null +++ b/plugins/webauthn/locales/translated/vi_VN.json @@ -0,0 +1,18 @@ +{ + "title": "Passkeys", + "signIn": "Đăng nhập bằng khóa truy cập", + "loginFailed": "Đăng nhập bằng passkey thất bại", + "description": "Sử dụng thông tin đăng nhập WebAuthn/FIDO2 để đăng nhập không cần mật khẩu", + "namePlaceholder": "Tên passkey", + "uv": { + "preferred": "Ưu tiên", + "required": "Bắt buộc", + "discouraged": "Không khuyến khích" + }, + "add": "Thêm Passkey", + "none": "Chưa đăng ký passkey nào.", + "added": "Đã thêm Passkey", + "addFailed": "Không thêm được Passkey", + "deleted": "Đã xóa Passkey", + "deleteFailed": "Xóa passkey thất bại" +} diff --git a/plugins/webauthn/locales/translated/zh_CN.json b/plugins/webauthn/locales/translated/zh_CN.json new file mode 100644 index 000000000..d101f9cde --- /dev/null +++ b/plugins/webauthn/locales/translated/zh_CN.json @@ -0,0 +1,18 @@ +{ + "title": "通行密钥", + "signIn": "使用通行密钥登录", + "loginFailed": "通行密钥登录失败", + "description": "使用 WebAuthn/FIDO2 凭据进行无密码登录", + "namePlaceholder": "通行密钥名称", + "uv": { + "preferred": "首选", + "required": "必需", + "discouraged": "不鼓励" + }, + "add": "添加通行密钥", + "none": "未注册通行密钥。", + "added": "通行密钥已添加", + "addFailed": "添加通行密钥失败", + "deleted": "通行密钥已删除", + "deleteFailed": "删除通行密钥失败" +} diff --git a/plugins/webauthn/locales/translated/zh_TW.json b/plugins/webauthn/locales/translated/zh_TW.json new file mode 100644 index 000000000..0f666fa4e --- /dev/null +++ b/plugins/webauthn/locales/translated/zh_TW.json @@ -0,0 +1,18 @@ +{ + "title": "通行金鑰", + "signIn": "使用通行密鑰登入", + "loginFailed": "Passkey 登入失敗", + "description": "使用 WebAuthn/FIDO2 憑證無需密碼即可登入", + "namePlaceholder": "通行金鑰名稱", + "uv": { + "preferred": "偏好", + "required": "需要", + "discouraged": "不鼓勵" + }, + "add": "新增通行金鑰", + "none": "尚未註冊通行金鑰。", + "added": "通行金鑰已新增", + "addFailed": "無法新增通行金鑰", + "deleted": "通行金鑰已刪除", + "deleteFailed": "刪除通行密鑰失敗" +} diff --git a/plugins/webauthn/manifest.json b/plugins/webauthn/manifest.json new file mode 100644 index 000000000..9135bc461 --- /dev/null +++ b/plugins/webauthn/manifest.json @@ -0,0 +1,31 @@ +{ + "id": "webauthn", + "name": "Passkeys", + "version": "1.0.0", + "description": "Sign in with a passkey or security key instead of a password. A passkey that checks your PIN or fingerprint also counts as the second factor.", + "author": { + "name": "Termix" + }, + "license": "MIT", + "repository": "https://github.com/Termix-SSH/Termix", + "category": "Access & Security", + "icon": "Fingerprint", + "engine": { + "termix": ">=2.9.0", + "api": "1" + }, + "capabilities": [ + "db:own", + "network:serve", + "auth:provide", + "settings:read-core", + "ui:surface" + ], + "contributes": { + "auth": { + "loginMethods": ["passkey"] + } + }, + "locales": "locales", + "platforms": ["linux", "win32", "darwin"] +} diff --git a/plugins/webauthn/migrations/mysql/0001_adopt_webauthn_credentials.sql b/plugins/webauthn/migrations/mysql/0001_adopt_webauthn_credentials.sql new file mode 100644 index 000000000..640b38281 --- /dev/null +++ b/plugins/webauthn/migrations/mysql/0001_adopt_webauthn_credentials.sql @@ -0,0 +1,19 @@ +-- webauthn 0001: adopt_webauthn_credentials +-- Generated by termix-plugin migrations. Review before committing. + +CREATE TABLE IF NOT EXISTS `webauthn_credentials` ( + `id` varchar(255) PRIMARY KEY NOT NULL, + `user_id` varchar(255) NOT NULL, + `name` text NOT NULL, + `credential_id` varchar(255) NOT NULL UNIQUE, + `public_key` text NOT NULL, + `counter` int NOT NULL DEFAULT 0, + `device_type` text, + `backed_up` boolean NOT NULL DEFAULT false, + `transports` text, + `user_verification` text NOT NULL DEFAULT ('preferred'), + `created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP), + `last_used_at` text, + FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +); +RENAME TABLE `webauthn_credentials` TO `p_webauthn_credentials`; diff --git a/plugins/webauthn/migrations/postgres/0001_adopt_webauthn_credentials.sql b/plugins/webauthn/migrations/postgres/0001_adopt_webauthn_credentials.sql new file mode 100644 index 000000000..6f31bea42 --- /dev/null +++ b/plugins/webauthn/migrations/postgres/0001_adopt_webauthn_credentials.sql @@ -0,0 +1,19 @@ +-- webauthn 0001: adopt_webauthn_credentials +-- Generated by termix-plugin migrations. Review before committing. + +CREATE TABLE IF NOT EXISTS "webauthn_credentials" ( + "id" varchar(255) PRIMARY KEY NOT NULL, + "user_id" varchar(255) NOT NULL, + "name" text NOT NULL, + "credential_id" varchar(255) NOT NULL UNIQUE, + "public_key" text NOT NULL, + "counter" integer NOT NULL DEFAULT 0, + "device_type" text, + "backed_up" boolean NOT NULL DEFAULT false, + "transports" text, + "user_verification" text NOT NULL DEFAULT 'preferred', + "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + "last_used_at" text, + FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE +); +ALTER TABLE "webauthn_credentials" RENAME TO "p_webauthn_credentials"; diff --git a/plugins/webauthn/migrations/snapshot.json b/plugins/webauthn/migrations/snapshot.json new file mode 100644 index 000000000..d3dda8fa4 --- /dev/null +++ b/plugins/webauthn/migrations/snapshot.json @@ -0,0 +1,65 @@ +{ + "version": 1, + "tables": { + "credentials": { + "columns": { + "id": { + "type": "varchar", + "length": 255, + "primaryKey": true, + "notNull": true + }, + "userId": { + "type": "refUser", + "length": 255, + "notNull": true + }, + "name": { + "type": "text", + "notNull": true + }, + "credentialId": { + "type": "varchar", + "length": 255, + "notNull": true, + "unique": true + }, + "publicKey": { + "type": "text", + "notNull": true + }, + "counter": { + "type": "integer", + "notNull": true, + "defaultValue": 0 + }, + "deviceType": { + "type": "text" + }, + "backedUp": { + "type": "boolean", + "notNull": true, + "defaultValue": false + }, + "transports": { + "type": "text" + }, + "userVerification": { + "type": "text", + "notNull": true, + "defaultValue": "preferred" + }, + "createdAt": { + "type": "timestamp", + "notNull": true, + "defaultNow": true + }, + "lastUsedAt": { + "type": "timestamp" + } + }, + "indexes": [], + "adopts": "webauthn_credentials" + } + } +} diff --git a/plugins/webauthn/migrations/sqlite/0001_adopt_webauthn_credentials.sql b/plugins/webauthn/migrations/sqlite/0001_adopt_webauthn_credentials.sql new file mode 100644 index 000000000..4440c8794 --- /dev/null +++ b/plugins/webauthn/migrations/sqlite/0001_adopt_webauthn_credentials.sql @@ -0,0 +1,19 @@ +-- webauthn 0001: adopt_webauthn_credentials +-- Generated by termix-plugin migrations. Review before committing. + +CREATE TABLE IF NOT EXISTS "webauthn_credentials" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "name" text NOT NULL, + "credential_id" text NOT NULL UNIQUE, + "public_key" text NOT NULL, + "counter" integer NOT NULL DEFAULT 0, + "device_type" text, + "backed_up" integer NOT NULL DEFAULT 0, + "transports" text, + "user_verification" text NOT NULL DEFAULT 'preferred', + "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, + "last_used_at" text, + FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE +); +ALTER TABLE "webauthn_credentials" RENAME TO "p_webauthn_credentials"; diff --git a/plugins/webauthn/package.json b/plugins/webauthn/package.json new file mode 100644 index 000000000..2a6d311e7 --- /dev/null +++ b/plugins/webauthn/package.json @@ -0,0 +1,19 @@ +{ + "name": "@termix-plugin/webauthn", + "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": { + "@simplewebauthn/browser": "^13.3.0", + "@simplewebauthn/server": "^13.3.2" + }, + "devDependencies": { + "@termix/plugin-sdk": "*" + } +} diff --git a/plugins/webauthn/src/backend/index.ts b/plugins/webauthn/src/backend/index.ts new file mode 100644 index 000000000..7d9225f58 --- /dev/null +++ b/plugins/webauthn/src/backend/index.ts @@ -0,0 +1,33 @@ +import type { Router } from "express"; +import type { PluginContext } from "@termix/plugin-sdk/backend"; +import { credentials } from "./tables.js"; +import { createCredentialRepository } from "./repository.js"; +import { createPasskeyService } from "./passkeys.js"; +import { PUBLIC_PATHS, registerPasskeyRoutes } from "./routes.js"; + +export async function activate(ctx: PluginContext) { + const table = await ctx.db.define(credentials); + const repository = createCredentialRepository(ctx.db, table); + const passkeys = createPasskeyService(ctx, repository); + + // No describe: a passkey only helps a user who already registered one, so + // it never counts for the password lockout guard. + ctx.auth.registerLoginMethod({ + id: "passkey", + labelKey: "signIn", + icon: "fingerprint", + kind: "form", + verify: (request) => passkeys.verifyLogin(request.body ?? {}), + }); + + registerPasskeyRoutes( + ctx.http.router({ public: PUBLIC_PATHS }), + ctx, + repository, + passkeys, + ); +} + +export async function deactivate() { + // Everything above was registered through ctx and is disposed by core. +} diff --git a/plugins/webauthn/src/backend/passkeys.ts b/plugins/webauthn/src/backend/passkeys.ts new file mode 100644 index 000000000..6970a3dcf --- /dev/null +++ b/plugins/webauthn/src/backend/passkeys.ts @@ -0,0 +1,298 @@ +import { randomUUID } from "node:crypto"; +import type { + AuthenticationResponseJSON, + AuthenticatorTransportFuture, + Base64URLString, + RegistrationResponseJSON, + WebAuthnCredential, +} from "@simplewebauthn/server"; +import { + generateAuthenticationOptions, + generateRegistrationOptions, + verifyAuthenticationResponse, + verifyRegistrationResponse, +} from "@simplewebauthn/server"; +import { + LoginMethodError, + type PluginContext, + type PluginVerifiedIdentity, +} from "@termix/plugin-sdk/backend"; +import type { CredentialRecord, CredentialRepository } from "./repository.js"; + +export type UserVerification = "discouraged" | "preferred" | "required"; + +interface ChallengeRecord { + challenge: string; + userId?: string; + rpID: string; + origin: string; + userVerification: UserVerification; + createdAt: number; +} + +const CHALLENGE_TTL_MS = 5 * 60 * 1000; + +export function normalizeUserVerification(value: unknown): UserVerification { + return value === "discouraged" || value === "required" ? value : "preferred"; +} + +function header( + headers: Record, + name: string, +): string | undefined { + const value = headers[name]; + if (Array.isArray(value)) return value[0]; + return typeof value === "string" ? value : undefined; +} + +/** The browser's origin, or the one a proxy reports. */ +export function requestOrigin(headers: Record): string { + const origin = header(headers, "origin"); + if (origin) return origin; + const proto = header(headers, "x-forwarded-proto") || "http"; + const host = + header(headers, "x-forwarded-host") || + header(headers, "host") || + "localhost"; + return `${proto.split(",")[0]}://${host.split(",")[0]}`; +} + +export function parseTransports( + value: string | null, +): AuthenticatorTransportFuture[] { + if (!value) return []; + try { + const parsed = JSON.parse(value); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +} + +function toCredential(record: CredentialRecord): WebAuthnCredential { + return { + id: record.credentialId as Base64URLString, + publicKey: Uint8Array.from( + Buffer.from(record.publicKey, "base64url"), + ) as WebAuthnCredential["publicKey"], + counter: record.counter, + transports: parseTransports(record.transports), + }; +} + +export type PasskeyService = ReturnType; + +/** + * Registration and sign-in ceremonies. Challenges live in memory for five + * minutes and are made in activate, so a disabled plugin forgets them. + */ +export function createPasskeyService( + ctx: PluginContext, + repository: CredentialRepository, +) { + const registrations = new Map(); + const authentications = new Map(); + + const prune = (map: Map) => { + const now = Date.now(); + for (const [id, record] of map) { + if (now - record.createdAt > CHALLENGE_TTL_MS) map.delete(id); + } + }; + const put = ( + map: Map, + record: Omit, + ) => { + prune(map); + const id = randomUUID(); + map.set(id, { ...record, createdAt: Date.now() }); + return id; + }; + const take = (map: Map, id: unknown) => { + if (typeof id !== "string") return null; + prune(map); + const record = map.get(id) ?? null; + map.delete(id); + return record; + }; + + async function rpName(): Promise { + try { + return (await ctx.settings.readCore("app_name")) || "Termix"; + } catch { + return "Termix"; + } + } + + return { + async registrationOptions(input: { + userId: string; + username: string; + origin: string; + userVerification: UserVerification; + }) { + const rpID = new URL(input.origin).hostname; + const existing = await repository.listByUserId(input.userId); + const options = await generateRegistrationOptions({ + rpName: await rpName(), + rpID, + userID: Buffer.from(input.userId, "utf8"), + userName: input.username, + userDisplayName: input.username, + attestationType: "none", + excludeCredentials: existing.map((credential) => ({ + id: credential.credentialId as Base64URLString, + transports: parseTransports(credential.transports), + })), + authenticatorSelection: { + residentKey: "required", + userVerification: input.userVerification, + }, + }); + const challengeId = put(registrations, { + challenge: options.challenge, + userId: input.userId, + rpID, + origin: input.origin, + userVerification: input.userVerification, + }); + return { options, challengeId }; + }, + + /** Stores the passkey. False when the challenge or response is bad. */ + async register(input: { + userId: string; + challengeId: unknown; + response: RegistrationResponseJSON | undefined; + name: unknown; + }): Promise { + const challenge = take(registrations, input.challengeId); + if (!challenge || challenge.userId !== input.userId) return "expired"; + if (!input.response) return false; + + const verification = await verifyRegistrationResponse({ + response: input.response, + expectedChallenge: challenge.challenge, + expectedOrigin: challenge.origin, + expectedRPID: challenge.rpID, + requireUserVerification: challenge.userVerification === "required", + }); + if (!verification.verified) return false; + + const { credential, credentialDeviceType, credentialBackedUp } = + verification.registrationInfo; + const name = + typeof input.name === "string" && input.name.trim() + ? input.name.trim().slice(0, 80) + : "Passkey"; + await repository.create({ + id: randomUUID(), + userId: input.userId, + name, + credentialId: credential.id, + publicKey: Buffer.from(credential.publicKey).toString("base64url"), + counter: credential.counter, + deviceType: credentialDeviceType, + backedUp: credentialBackedUp, + transports: JSON.stringify(input.response.response?.transports ?? []), + userVerification: challenge.userVerification, + }); + return true; + }, + + async authenticationOptions(input: { + userId?: string; + origin: string; + userVerification: UserVerification; + }) { + const rpID = new URL(input.origin).hostname; + let allowCredentials: + | { id: Base64URLString; transports?: AuthenticatorTransportFuture[] }[] + | undefined; + if (input.userId) { + const credentials = await repository.listByUserId(input.userId); + if (credentials.length === 0) return null; + allowCredentials = credentials.map((credential) => ({ + id: credential.credentialId as Base64URLString, + transports: parseTransports(credential.transports), + })); + } + const options = await generateAuthenticationOptions({ + rpID, + allowCredentials, + userVerification: input.userVerification, + }); + const challengeId = put(authentications, { + challenge: options.challenge, + userId: input.userId, + rpID, + origin: input.origin, + userVerification: input.userVerification, + }); + return { options, challengeId }; + }, + + /** + * Checks an assertion and says who signed in. A passkey that verified + * the user (PIN or biometric) counts as the second factor too. + */ + async verifyLogin( + body: Record, + ): Promise { + const challenge = take(authentications, body.challengeId); + if (!challenge) { + throw new LoginMethodError("Authentication challenge expired", 400); + } + const response = body.response as AuthenticationResponseJSON | undefined; + if (!response?.id) { + throw new LoginMethodError("Invalid passkey response", 400); + } + const credential = await repository.findByCredentialId(response.id); + if ( + !credential || + (challenge.userId && challenge.userId !== credential.userId) + ) { + throw new LoginMethodError("Passkey not recognized", 401); + } + + let verification: Awaited< + ReturnType + >; + try { + verification = await verifyAuthenticationResponse({ + response, + expectedChallenge: challenge.challenge, + expectedOrigin: challenge.origin, + expectedRPID: challenge.rpID, + credential: toCredential(credential), + requireUserVerification: challenge.userVerification === "required", + advancedFIDOConfig: { userVerification: challenge.userVerification }, + }); + } catch (error) { + ctx.log.warn( + `Passkey authentication failed for ${credential.userId}: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + throw new LoginMethodError("Passkey authentication failed", 401); + } + if (!verification.verified) { + throw new LoginMethodError("Passkey authentication failed", 401); + } + + await repository.updateAuthState(credential.id, { + counter: verification.authenticationInfo.newCounter, + backedUp: verification.authenticationInfo.credentialBackedUp, + deviceType: verification.authenticationInfo.credentialDeviceType, + lastUsedAt: new Date().toISOString(), + }); + + return { + kind: "user", + userId: credential.userId, + mfaSatisfied: verification.authenticationInfo.userVerified === true, + rememberMe: !!body.rememberMe, + }; + }, + }; +} diff --git a/plugins/webauthn/src/backend/repository.ts b/plugins/webauthn/src/backend/repository.ts new file mode 100644 index 000000000..a00e6f20d --- /dev/null +++ b/plugins/webauthn/src/backend/repository.ts @@ -0,0 +1,83 @@ +import { and, eq } from "drizzle-orm"; +import type { PluginDatabase } from "@termix/plugin-sdk/backend"; + +export interface CredentialRecord { + id: string; + userId: string; + name: string; + credentialId: string; + publicKey: string; + counter: number; + deviceType: string | null; + backedUp: boolean; + transports: string | null; + userVerification: string; + createdAt: string; + lastUsedAt: string | null; +} + +/* 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 */ + +export type CredentialRepository = ReturnType< + typeof createCredentialRepository +>; + +export function createCredentialRepository(db: PluginDatabase, table: Table) { + const client = () => db.client(); + + return { + async listByUserId(userId: string): Promise { + const drizzle = await client(); + return drizzle.select().from(table).where(eq(table.userId, userId)); + }, + + async findByCredentialId( + credentialId: string, + ): Promise { + const drizzle = await client(); + const rows = await drizzle + .select() + .from(table) + .where(eq(table.credentialId, credentialId)) + .limit(1); + return (rows[0] as CredentialRecord) ?? null; + }, + + async create( + input: Omit, + ): Promise { + const drizzle = await client(); + await drizzle + .insert(table) + .values({ ...input, createdAt: new Date().toISOString() }); + await db.persist(); + }, + + async updateAuthState( + id: string, + state: { + counter: number; + backedUp: boolean; + deviceType: string; + lastUsedAt: string; + }, + ): Promise { + const drizzle = await client(); + await drizzle.update(table).set(state).where(eq(table.id, id)); + await db.persist(); + }, + + async deleteForUser(userId: string, id: string): Promise { + const drizzle = await client(); + await drizzle + .delete(table) + .where(and(eq(table.id, id), eq(table.userId, userId))); + await db.persist(); + }, + }; +} diff --git a/plugins/webauthn/src/backend/routes.ts b/plugins/webauthn/src/backend/routes.ts new file mode 100644 index 000000000..278f3caa1 --- /dev/null +++ b/plugins/webauthn/src/backend/routes.ts @@ -0,0 +1,193 @@ +import type { Request, Response, Router } from "express"; +import { eq } from "drizzle-orm"; +import type { RegistrationResponseJSON } from "@simplewebauthn/server"; +import type { PluginContext } from "@termix/plugin-sdk/backend"; +import { + normalizeUserVerification, + parseTransports, + requestOrigin, + type PasskeyService, +} from "./passkeys.js"; +import type { CredentialRepository } from "./repository.js"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +type Table = any; +type Drizzle = any; +/* eslint-enable @typescript-eslint/no-explicit-any */ + +export const PUBLIC_PATHS = ["/authenticate/options"]; + +export function registerPasskeyRoutes( + router: Router, + ctx: PluginContext, + repository: CredentialRepository, + passkeys: PasskeyService, +): void { + async function findUser( + column: "id" | "username", + value: string, + ): Promise<{ id: string; username: string } | null> { + const { users } = await ctx.db.refs<{ users: Table }>(); + const drizzle = await ctx.db.client(); + const rows = await drizzle + .select({ id: users.id, username: users.username }) + .from(users) + .where(eq(users[column], value)) + .limit(1); + return rows[0] ?? null; + } + + /** + * @openapi + * /plugin-api/webauthn/credentials: + * get: + * summary: List passkeys + * description: Lists the caller's registered passkeys. + * tags: + * - Passkeys + * responses: + * 200: + * description: List of passkeys. + */ + router.get("/credentials", async (_req: Request, res: Response) => { + const credentials = await repository.listByUserId(ctx.currentActor()!); + res.json({ + credentials: credentials.map((credential) => ({ + id: credential.id, + name: credential.name, + deviceType: credential.deviceType, + backedUp: !!credential.backedUp, + transports: parseTransports(credential.transports), + userVerification: credential.userVerification, + createdAt: credential.createdAt, + lastUsedAt: credential.lastUsedAt, + })), + }); + }); + + /** + * @openapi + * /plugin-api/webauthn/register/options: + * post: + * summary: Start passkey registration + * tags: + * - Passkeys + * responses: + * 200: + * description: Registration options and challenge id. + * 404: + * description: User not found. + */ + router.post("/register/options", async (req: Request, res: Response) => { + const user = await findUser("id", ctx.currentActor()!); + if (!user) return res.status(404).json({ error: "User not found" }); + res.json( + await passkeys.registrationOptions({ + userId: user.id, + username: user.username, + origin: requestOrigin(req.headers), + userVerification: normalizeUserVerification(req.body?.userVerification), + }), + ); + }); + + /** + * @openapi + * /plugin-api/webauthn/register/verify: + * post: + * summary: Finish passkey registration + * tags: + * - Passkeys + * responses: + * 200: + * description: Passkey registered. + * 400: + * description: Registration failed or challenge expired. + */ + router.post("/register/verify", async (req: Request, res: Response) => { + const userId = ctx.currentActor()!; + try { + const result = await passkeys.register({ + userId, + challengeId: req.body?.challengeId, + response: req.body?.response as RegistrationResponseJSON | undefined, + name: req.body?.name, + }); + if (result === "expired") { + return res + .status(400) + .json({ error: "Registration challenge expired" }); + } + if (!result) { + return res.status(400).json({ error: "Passkey registration failed" }); + } + res.json({ success: true }); + } catch (error) { + ctx.log.warn( + `Passkey registration failed for ${userId}: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + res.status(400).json({ error: "Passkey registration failed" }); + } + }); + + /** + * @openapi + * /plugin-api/webauthn/authenticate/options: + * post: + * summary: Start passkey login + * description: Public. Authentication options, optionally scoped to a username. The assertion is then sent to /users/auth/passkey/verify. + * tags: + * - Passkeys + * responses: + * 200: + * description: Authentication options and challenge id. + * 404: + * description: No passkeys found for the user. + */ + router.post("/authenticate/options", async (req: Request, res: Response) => { + const username = + typeof req.body?.username === "string" ? req.body.username.trim() : ""; + let userId: string | undefined; + if (username) { + const user = await findUser("username", username); + if (!user) return res.status(404).json({ error: "No passkeys found" }); + userId = user.id; + } + const result = await passkeys.authenticationOptions({ + userId, + origin: requestOrigin(req.headers), + userVerification: normalizeUserVerification(req.body?.userVerification), + }); + if (!result) return res.status(404).json({ error: "No passkeys found" }); + res.json(result); + }); + + /** + * @openapi + * /plugin-api/webauthn/credentials/{credentialId}: + * delete: + * summary: Delete a passkey + * tags: + * - Passkeys + * parameters: + * - in: path + * name: credentialId + * required: true + * schema: { type: string } + * responses: + * 200: + * description: Passkey deleted. + */ + router.delete( + "/credentials/:credentialId", + async (req: Request, res: Response) => { + await repository.deleteForUser( + ctx.currentActor()!, + String(req.params.credentialId), + ); + res.json({ success: true }); + }, + ); +} diff --git a/plugins/webauthn/src/backend/tables.ts b/plugins/webauthn/src/backend/tables.ts new file mode 100644 index 000000000..b05242c88 --- /dev/null +++ b/plugins/webauthn/src/backend/tables.ts @@ -0,0 +1,34 @@ +import { + adoptLegacyTable, + boolean, + defineTable, + integer, + refUser, + text, + timestamp, + varchar, +} from "@termix/plugin-sdk/db"; + +/** + * Adopted from core's webauthn_credentials, so the column names are the + * legacy ones and the rename carries every passkey across. + */ +export const credentials = adoptLegacyTable( + "webauthn_credentials", + defineTable("credentials", { + id: varchar().primaryKey(), + userId: refUser(), + name: text().notNull(), + credentialId: varchar().notNull().unique(), + publicKey: text().notNull(), + counter: integer().notNull().default(0), + deviceType: text(), + backedUp: boolean().notNull().default(false), + transports: text(), + userVerification: text().notNull().default("preferred"), + createdAt: timestamp().notNull().defaultNow(), + lastUsedAt: timestamp(), + }), +); + +export const tables = [credentials]; diff --git a/plugins/webauthn/src/frontend/PasskeyEnrollment.tsx b/plugins/webauthn/src/frontend/PasskeyEnrollment.tsx new file mode 100644 index 000000000..e9ad8aa33 --- /dev/null +++ b/plugins/webauthn/src/frontend/PasskeyEnrollment.tsx @@ -0,0 +1,137 @@ +import { useEffect, useMemo, useState } from "react"; +import { KeyRound, Trash2 } from "lucide-react"; +import { toast } from "sonner"; +import { Button, Input, Select2 } from "@termix/plugin-sdk/ui"; +import { usePluginApi, useTranslation } from "@termix/plugin-sdk/frontend"; +import { + createWebAuthnApi, + type WebAuthnCredentialSummary, + type WebAuthnUserVerification, +} from "./webauthn-api"; + +function apiErrorMessage(error: unknown, fallback: string): string { + const data = (error as { response?: { data?: { error?: unknown } } }) + ?.response?.data; + return typeof data?.error === "string" && data.error ? data.error : fallback; +} + +/** Settings > Security: register and remove passkeys. */ +export function PasskeyEnrollment() { + const { t } = useTranslation(); + const pluginApi = usePluginApi(); + const api = useMemo(() => createWebAuthnApi(pluginApi), [pluginApi]); + const [passkeys, setPasskeys] = useState([]); + const [loading, setLoading] = useState(false); + const [name, setName] = useState(""); + const [userVerification, setUserVerification] = + useState("preferred"); + + useEffect(() => { + api + .list() + .then(setPasskeys) + .catch(() => {}); + }, [api]); + + async function add() { + setLoading(true); + try { + await api.register(name || t("defaultName"), userVerification); + setPasskeys(await api.list()); + setName(""); + toast.success(t("added")); + } catch (e) { + toast.error(apiErrorMessage(e, t("addFailed"))); + } finally { + setLoading(false); + } + } + + async function remove(id: string) { + setLoading(true); + try { + await api.remove(id); + setPasskeys((prev) => prev.filter((item) => item.id !== id)); + toast.success(t("deleted")); + } catch (e) { + toast.error(apiErrorMessage(e, t("deleteFailed"))); + } finally { + setLoading(false); + } + } + + return ( +
+ + {t("description")} + + +
+ setName(e.target.value)} + className="h-8 text-xs" + disabled={loading} + /> + + setUserVerification(e.target.value as WebAuthnUserVerification) + } + className="h-8 w-28 text-xs border border-border bg-background px-2 outline-none focus:ring-1 focus:ring-ring" + disabled={loading} + > + + + + +
+ + +
+ {passkeys.length === 0 ? ( +
+ {t("none")} +
+ ) : ( + passkeys.map((passkey) => ( +
+
+ + {passkey.name} + + + {passkey.deviceType || t("unknownDevice")} + {passkey.backedUp ? ` / ${t("synced")}` : ""} + +
+ +
+ )) + )} +
+
+ ); +} diff --git a/plugins/webauthn/src/frontend/PasskeyLoginButton.tsx b/plugins/webauthn/src/frontend/PasskeyLoginButton.tsx new file mode 100644 index 000000000..599502148 --- /dev/null +++ b/plugins/webauthn/src/frontend/PasskeyLoginButton.tsx @@ -0,0 +1,59 @@ +import { useMemo, useState } from "react"; +import { Fingerprint } from "lucide-react"; +import { toast } from "sonner"; +import { Button } from "@termix/plugin-sdk/ui"; +import { + usePluginApi, + useTranslation, + type LoginMethodUIProps, +} from "@termix/plugin-sdk/frontend"; +import { createWebAuthnApi, isPasskeySupported } from "./webauthn-api"; + +export function PasskeyLoginButton({ + disabled, + submit, + username, +}: LoginMethodUIProps) { + const { t } = useTranslation(); + const pluginApi = usePluginApi(); + const api = useMemo(() => createWebAuthnApi(pluginApi), [pluginApi]); + const [loading, setLoading] = useState(false); + const [supported] = useState(isPasskeySupported); + if (!supported) return null; + + return ( + + ); +} diff --git a/plugins/webauthn/src/frontend/index.tsx b/plugins/webauthn/src/frontend/index.tsx new file mode 100644 index 000000000..e71c24b4d --- /dev/null +++ b/plugins/webauthn/src/frontend/index.tsx @@ -0,0 +1,13 @@ +import type { TermixApp } from "@termix/plugin-sdk/frontend"; +import { PasskeyLoginButton } from "./PasskeyLoginButton"; +import { PasskeyEnrollment } from "./PasskeyEnrollment"; + +export function activate(app: TermixApp): void { + app.registerLoginMethod({ + id: "passkey", + titleKey: "title", + placement: "inline", + component: PasskeyLoginButton, + enrollment: PasskeyEnrollment, + }); +} diff --git a/plugins/webauthn/src/frontend/webauthn-api.ts b/plugins/webauthn/src/frontend/webauthn-api.ts new file mode 100644 index 000000000..7837af4b5 --- /dev/null +++ b/plugins/webauthn/src/frontend/webauthn-api.ts @@ -0,0 +1,82 @@ +import type { + AuthenticationResponseJSON, + PublicKeyCredentialCreationOptionsJSON, + PublicKeyCredentialRequestOptionsJSON, + RegistrationResponseJSON, +} from "@simplewebauthn/browser"; +import { + browserSupportsWebAuthn, + startAuthentication, + startRegistration, +} from "@simplewebauthn/browser"; +import type { PluginApiClient } from "@termix/plugin-sdk/frontend"; + +export type WebAuthnUserVerification = "discouraged" | "preferred" | "required"; + +export type WebAuthnCredentialSummary = { + id: string; + name: string; + deviceType?: string | null; + backedUp: boolean; + transports: string[]; + userVerification: WebAuthnUserVerification; + createdAt: string; + lastUsedAt?: string | null; +}; + +type OptionsResponse = { options: T; challengeId: string }; + +export function isPasskeySupported(): boolean { + try { + return browserSupportsWebAuthn(); + } catch { + return false; + } +} + +export function createWebAuthnApi(api: PluginApiClient) { + return { + /** + * Runs the browser ceremony and returns the body for the login method's + * verify step. Without a username, discoverable passkeys still work. + */ + async passkeyAssertion(username?: string) { + const { data } = await api.post< + OptionsResponse + >("authenticate/options", username ? { username } : {}); + const response = await startAuthentication({ + optionsJSON: data.options, + }); + return { + challengeId: data.challengeId, + response: response as AuthenticationResponseJSON, + }; + }, + + async list(): Promise { + const { data } = await api.get<{ + credentials?: WebAuthnCredentialSummary[]; + }>("credentials"); + return data.credentials ?? []; + }, + + async register( + name: string, + userVerification: WebAuthnUserVerification, + ): Promise { + const { data } = await api.post< + OptionsResponse + >("register/options", { userVerification }); + const response = await startRegistration({ optionsJSON: data.options }); + await api.post("register/verify", { + challengeId: data.challengeId, + name, + response: response as RegistrationResponseJSON, + }); + }, + + async remove(id: string): Promise { + await api.delete(`credentials/${encodeURIComponent(id)}`); + }, + }; +} diff --git a/plugins/webauthn/tests/backend/helpers.ts b/plugins/webauthn/tests/backend/helpers.ts new file mode 100644 index 000000000..a5d036ec1 --- /dev/null +++ b/plugins/webauthn/tests/backend/helpers.ts @@ -0,0 +1,96 @@ +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 }, + // 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 }>; + close: () => Promise; +} + +/** + * The plugin activated against a real in-memory database, its router mounted + * on an express app the way core mounts it: JSON parsed, the acting user set + * from the request, everything else left to the plugin. + */ +export async function startServer( + options: { + users?: string[]; + capabilities?: string[]; + before?: (sqlite: TestDb["sqlite"]) => void; + } = {}, +): Promise { + const db = await createTestDb(pluginDir, { before: options.before }); + for (const user of options.users ?? ["user-1", "user-2"]) { + db.sqlite + .prepare("INSERT INTO users (id, username) VALUES (?, ?)") + .run(user, user); + } + + let router: Router | null = null; + const mock = createMockCtx({ + pluginId: manifest.id, + manifest, + capabilities: options.capabilities ?? manifest.capabilities, + db: db.database, + router: () => (router = express.Router()), + coreSettings: { app_name: "Termix Test" }, + }); + + const { activate } = await import("../../src/backend/index.js"); + await activate(mock.ctx); + + const app = express(); + app.use(express.json()); + 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 = "user-1", body: given } = {}) { + const body = method === "GET" ? undefined : given; + const response = await fetch(`http://127.0.0.1:${port}${path}`, { + method, + headers: { + "x-test-user": user, + origin: "https://termix.test", + ...(body !== undefined ? { "content-type": "application/json" } : {}), + }, + body: body !== undefined ? JSON.stringify(body) : undefined, + }); + const text = await response.text(); + return { status: response.status, body: text ? JSON.parse(text) : null }; + }, + async close() { + await new Promise((resolve) => server.close(() => resolve())); + db.close(); + }, + }; +} diff --git a/plugins/webauthn/tests/backend/passkeys.test.ts b/plugins/webauthn/tests/backend/passkeys.test.ts new file mode 100644 index 000000000..e03d5f5e3 --- /dev/null +++ b/plugins/webauthn/tests/backend/passkeys.test.ts @@ -0,0 +1,371 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const webauthn = vi.hoisted(() => ({ + verifyRegistration: vi.fn(), + verifyAuthentication: vi.fn(), + registrationOptions: vi.fn(), + authenticationOptions: vi.fn(), +})); + +vi.mock("@simplewebauthn/server", () => ({ + generateRegistrationOptions: webauthn.registrationOptions, + generateAuthenticationOptions: webauthn.authenticationOptions, + verifyRegistrationResponse: webauthn.verifyRegistration, + verifyAuthenticationResponse: webauthn.verifyAuthentication, +})); + +import { startServer, type TestServer } from "./helpers"; +import { requestOrigin } from "../../src/backend/passkeys.js"; + +let server: TestServer | null = null; + +beforeEach(() => { + vi.clearAllMocks(); + webauthn.registrationOptions.mockImplementation(async (input) => ({ + challenge: "reg-challenge", + rp: { name: input.rpName, id: input.rpID }, + })); + webauthn.authenticationOptions.mockImplementation(async (input) => ({ + challenge: "auth-challenge", + allowCredentials: input.allowCredentials, + })); + webauthn.verifyRegistration.mockResolvedValue({ + verified: true, + registrationInfo: { + credential: { + id: "cred-1", + publicKey: new Uint8Array([1, 2, 3]), + counter: 0, + }, + credentialDeviceType: "multiDevice", + credentialBackedUp: true, + }, + }); + webauthn.verifyAuthentication.mockResolvedValue({ + verified: true, + authenticationInfo: { + newCounter: 5, + userVerified: true, + credentialBackedUp: true, + credentialDeviceType: "multiDevice", + }, + }); +}); + +afterEach(async () => { + await server?.close(); + server = null; +}); + +function passkeyMethod(target: TestServer) { + const method = target.mock.auth.loginMethods.find( + (entry) => entry.id === "passkey", + ); + if (!method?.verify) throw new Error("passkey method not registered"); + return method; +} + +async function registerPasskey(target: TestServer, user = "user-1") { + const options = await target.request("POST", "/register/options", { + user, + body: { userVerification: "required" }, + }); + expect(options.status).toBe(200); + const verify = await target.request("POST", "/register/verify", { + user, + body: { + challengeId: options.body.challengeId, + name: "Laptop", + response: { id: "cred-1", response: { transports: ["internal"] } }, + }, + }); + expect(verify.status).toBe(200); +} + +async function loginWith( + target: TestServer, + body: Record = {}, +) { + const options = await target.request("POST", "/authenticate/options", { + user: "", + body, + }); + expect(options.status).toBe(200); + return passkeyMethod(target).verify!( + { + body: { + challengeId: options.body.challengeId, + response: { id: "cred-1" }, + }, + query: {}, + headers: {}, + }, + null, + ); +} + +describe("registration", () => { + it("registers a passkey for the caller and lists it", async () => { + server = await startServer(); + await registerPasskey(server); + + expect(webauthn.registrationOptions).toHaveBeenCalledWith( + expect.objectContaining({ + rpName: "Termix Test", + rpID: "termix.test", + userName: "user-1", + }), + ); + const list = await server.request("GET", "/credentials"); + expect(list.body.credentials).toEqual([ + expect.objectContaining({ + name: "Laptop", + deviceType: "multiDevice", + backedUp: true, + transports: ["internal"], + userVerification: "required", + }), + ]); + expect( + (await server.request("GET", "/credentials", { user: "user-2" })).body + .credentials, + ).toEqual([]); + }); + + it("refuses a challenge issued to someone else", async () => { + server = await startServer(); + const options = await server.request("POST", "/register/options", { + body: {}, + }); + const verify = await server.request("POST", "/register/verify", { + user: "user-2", + body: { challengeId: options.body.challengeId, response: { id: "x" } }, + }); + expect(verify.status).toBe(400); + }); + + it("refuses a failed attestation", async () => { + server = await startServer(); + webauthn.verifyRegistration.mockResolvedValue({ verified: false }); + const options = await server.request("POST", "/register/options", { + body: {}, + }); + const verify = await server.request("POST", "/register/verify", { + body: { challengeId: options.body.challengeId, response: { id: "x" } }, + }); + expect(verify.status).toBe(400); + }); + + it("deletes only the caller's own passkey", async () => { + server = await startServer(); + await registerPasskey(server); + const [{ id }] = (await server.request("GET", "/credentials")).body + .credentials; + await server.request("DELETE", `/credentials/${id}`, { user: "user-2" }); + expect( + (await server.request("GET", "/credentials")).body.credentials, + ).toHaveLength(1); + await server.request("DELETE", `/credentials/${id}`); + expect( + (await server.request("GET", "/credentials")).body.credentials, + ).toHaveLength(0); + }); +}); + +describe("login", () => { + it("says who signed in and counts a verified user as the second factor", async () => { + server = await startServer(); + await registerPasskey(server); + const identity = await loginWith(server); + expect(identity).toMatchObject({ + kind: "user", + userId: "user-1", + mfaSatisfied: true, + }); + const row = server.db.sqlite + .prepare("SELECT counter, last_used_at FROM p_webauthn_credentials") + .get() as { counter: number; last_used_at: string | null }; + expect(row.counter).toBe(5); + expect(row.last_used_at).toBeTruthy(); + }); + + it("leaves the second factor to core when the user was not verified", async () => { + server = await startServer(); + await registerPasskey(server); + webauthn.verifyAuthentication.mockResolvedValue({ + verified: true, + authenticationInfo: { + newCounter: 1, + userVerified: false, + credentialBackedUp: false, + credentialDeviceType: "singleDevice", + }, + }); + expect(await loginWith(server)).toMatchObject({ mfaSatisfied: false }); + }); + + it("scopes the options to a username and 404s without passkeys", async () => { + server = await startServer(); + await registerPasskey(server); + const options = await server.request("POST", "/authenticate/options", { + user: "", + body: { username: "user-1" }, + }); + expect(options.body.options.allowCredentials).toEqual([ + { id: "cred-1", transports: ["internal"] }, + ]); + expect( + ( + await server.request("POST", "/authenticate/options", { + user: "", + body: { username: "user-2" }, + }) + ).status, + ).toBe(404); + }); + + it("refuses a passkey that belongs to another user than asked for", async () => { + server = await startServer(); + await registerPasskey(server); + const options = await server.request("POST", "/authenticate/options", { + user: "", + body: { username: "user-1" }, + }); + server.db.sqlite.exec( + "UPDATE p_webauthn_credentials SET user_id = 'user-2'", + ); + await expect( + passkeyMethod(server).verify!( + { + body: { + challengeId: options.body.challengeId, + response: { id: "cred-1" }, + }, + query: {}, + headers: {}, + }, + null, + ), + ).rejects.toMatchObject({ status: 401 }); + }); + + it("refuses an unknown passkey and a failed assertion", async () => { + server = await startServer(); + await expect(loginWith(server)).rejects.toMatchObject({ status: 401 }); + await registerPasskey(server); + webauthn.verifyAuthentication.mockRejectedValue(new Error("bad sig")); + await expect(loginWith(server)).rejects.toMatchObject({ status: 401 }); + }); + + it("uses a challenge only once", async () => { + server = await startServer(); + await registerPasskey(server); + const options = await server.request("POST", "/authenticate/options", { + user: "", + body: {}, + }); + const request = { + body: { + challengeId: options.body.challengeId, + response: { id: "cred-1" }, + }, + query: {}, + headers: {}, + }; + await passkeyMethod(server).verify!(request, null); + await expect( + passkeyMethod(server).verify!(request, null), + ).rejects.toMatchObject({ status: 400 }); + }); +}); + +describe("adoption", () => { + const LEGACY = ` + CREATE TABLE webauthn_credentials ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + name TEXT NOT NULL, + credential_id TEXT NOT NULL UNIQUE, + public_key TEXT NOT NULL, + counter INTEGER NOT NULL DEFAULT 0, + device_type TEXT, + backed_up INTEGER NOT NULL DEFAULT 0, + transports TEXT, + user_verification TEXT NOT NULL DEFAULT 'preferred', + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + last_used_at TEXT, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + );`; + + it("keeps existing passkeys, which still sign in", async () => { + server = await startServer({ + before: (sqlite) => { + sqlite + .prepare("INSERT INTO users (id, username) VALUES (?, ?)") + .run("old-user", "old-user"); + sqlite.exec(LEGACY); + sqlite.exec(`INSERT INTO webauthn_credentials + (id, user_id, name, credential_id, public_key, counter, transports) + VALUES ('p1', 'old-user', 'Old key', 'cred-1', 'AQID', 3, '[]')`); + }, + }); + const tables = server.db.sqlite + .prepare("SELECT name FROM sqlite_master WHERE type = 'table'") + .all() + .map((row) => (row as { name: string }).name); + expect(tables).toContain("p_webauthn_credentials"); + expect(tables).not.toContain("webauthn_credentials"); + expect(await loginWith(server)).toMatchObject({ userId: "old-user" }); + }); + + it("creates the table on a fresh install", async () => { + server = await startServer(); + expect( + server.db.sqlite + .prepare("SELECT COUNT(*) AS n FROM p_webauthn_credentials") + .get(), + ).toEqual({ n: 0 }); + }); +}); + +describe("capabilities", () => { + it("fails closed without auth:provide", async () => { + await expect( + startServer({ + capabilities: [ + "db:own", + "network:serve", + "settings:read-core", + "ui:surface", + ], + }), + ).rejects.toThrow(/auth:provide/); + }); + + it("fails closed without network:serve", async () => { + await expect( + startServer({ + capabilities: [ + "db:own", + "auth:provide", + "settings:read-core", + "ui:surface", + ], + }), + ).rejects.toThrow(/network:serve/); + }); +}); + +describe("requestOrigin", () => { + it("prefers Origin, then forwarded host and proto", () => { + expect(requestOrigin({ origin: "https://a.test" })).toBe("https://a.test"); + expect( + requestOrigin({ + "x-forwarded-proto": "https, http", + "x-forwarded-host": "b.test", + host: "internal", + }), + ).toBe("https://b.test"); + expect(requestOrigin({ host: "c.test:8080" })).toBe("http://c.test:8080"); + }); +}); diff --git a/plugins/webauthn/tests/frontend/activate.test.tsx b/plugins/webauthn/tests/frontend/activate.test.tsx new file mode 100644 index 000000000..a5d2495ed --- /dev/null +++ b/plugins/webauthn/tests/frontend/activate.test.tsx @@ -0,0 +1,129 @@ +import { afterEach, beforeEach, 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"; + +const browser = vi.hoisted(() => ({ + startAuthentication: vi.fn(async () => ({ id: "cred-1" })), + startRegistration: vi.fn(async () => ({ id: "cred-1" })), + browserSupportsWebAuthn: vi.fn(() => true), +})); +vi.mock("@simplewebauthn/browser", () => browser); + +import * as plugin from "../../src/frontend/index"; +import { createWebAuthnApi } from "../../src/frontend/webauthn-api"; +import manifestJson from "../../manifest.json"; +import locales from "../../locales/en.json"; + +const manifest = manifestJson as unknown as PluginManifest; + +let rendered: RenderedPluginApp | null = null; + +beforeEach(() => { + vi.clearAllMocks(); + browser.browserSupportsWebAuthn.mockReturnValue(true); +}); + +afterEach(async () => { + await rendered?.deactivate(); + rendered = null; +}); + +describe(`${manifest.id} activate`, () => { + it("registers the passkey login method", async () => { + rendered = await renderWithApp(plugin, { manifest, locales }); + expect(rendered.registered.loginMethods()).toEqual(["passkey"]); + }); + + it("runs the ceremony and submits the assertion", async () => { + const post = vi.fn(async () => ({ + data: { options: { challenge: "abc" }, challengeId: "chal-1" }, + })); + rendered = await renderWithApp(plugin, { + manifest, + locales, + api: { post } as never, + }); + const submit = vi.fn(async () => {}); + rendered.renderLoginMethod("passkey", { submit, username: " luke " }); + fireEvent.click(screen.getByText(locales.signIn)); + await waitFor(() => + expect(submit).toHaveBeenCalledWith({ + challengeId: "chal-1", + response: { id: "cred-1" }, + }), + ); + expect(post).toHaveBeenCalledWith("authenticate/options", { + username: "luke", + }); + }); + + it("draws nothing where the browser has no WebAuthn", async () => { + browser.browserSupportsWebAuthn.mockReturnValue(false); + rendered = await renderWithApp(plugin, { manifest, locales }); + rendered.renderLoginMethod("passkey"); + expect(screen.queryByText(locales.signIn)).toBeNull(); + }); + + it("lists passkeys in the enrolment section", async () => { + rendered = await renderWithApp(plugin, { + manifest, + locales, + api: { + get: vi.fn(async () => ({ + data: { + credentials: [ + { + id: "p1", + name: "Laptop", + backedUp: true, + transports: [], + userVerification: "preferred", + createdAt: "", + }, + ], + }, + })), + } as never, + }); + rendered.renderEnrollment("passkey"); + expect(await screen.findByText("Laptop")).toBeTruthy(); + }); + + it("removes the method on deactivate", async () => { + const app = await renderWithApp(plugin, { manifest, locales }); + await app.deactivate(); + expect(app.registered.loginMethods()).toEqual([]); + }); +}); + +describe("webauthn api", () => { + it("omits the username so discoverable passkeys work", async () => { + const post = vi.fn(async () => ({ + data: { options: { challenge: "abc" }, challengeId: "chal-2" }, + })); + await createWebAuthnApi({ post } as never).passkeyAssertion(); + expect(post).toHaveBeenCalledWith("authenticate/options", {}); + }); + + it("registers through options then verify", async () => { + const post = vi + .fn() + .mockResolvedValueOnce({ + data: { options: { challenge: "r" }, challengeId: "reg-1" }, + }) + .mockResolvedValueOnce({ data: { success: true } }); + await createWebAuthnApi({ post } as never).register("Key", "required"); + expect(post).toHaveBeenNthCalledWith(1, "register/options", { + userVerification: "required", + }); + expect(post).toHaveBeenNthCalledWith(2, "register/verify", { + challengeId: "reg-1", + name: "Key", + response: { id: "cred-1" }, + }); + }); +}); diff --git a/plugins/webauthn/tsconfig.json b/plugins/webauthn/tsconfig.json new file mode 100644 index 000000000..7b35314c3 --- /dev/null +++ b/plugins/webauthn/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/webauthn/vitest.config.ts b/plugins/webauthn/vitest.config.ts new file mode 100644 index 000000000..31ba004fc --- /dev/null +++ b/plugins/webauthn/vitest.config.ts @@ -0,0 +1,3 @@ +import { pluginVitestConfig } from "@termix/plugin-sdk/vitest-preset"; + +export default pluginVitestConfig(import.meta.url); diff --git a/src/backend/auth/core-auth.ts b/src/backend/auth/core-auth.ts index 54b84f250..00e8e3e5c 100644 --- a/src/backend/auth/core-auth.ts +++ b/src/backend/auth/core-auth.ts @@ -8,6 +8,7 @@ import { isTrustedProxyAuthEnabled } from "../utils/trusted-proxy-auth.js"; import { registerPasswordLoginMethod } from "./builtin-login-methods.js"; import { registerLegacyLoginProviders } from "./legacy-providers.js"; import { listLoginMethods } from "./registry.js"; +import { LoginMethodError } from "./types.js"; let registered = false; @@ -36,6 +37,29 @@ export function isPasswordLoginSettingOn(): boolean { } } +/** + * Refuses a new second-factor enrolment when core could not honour it: with + * trusted proxy login on the proxy decides who signs in, and with password + * login off users sign in through external methods that skip second factors + * by default. + */ +export function assertSecondFactorEnrollmentAllowed(): void { + if (isTrustedProxyAuthEnabled()) { + throw new LoginMethodError( + "Second factors are disabled while trusted proxy authentication is enabled", + 409, + "trusted_proxy_enabled", + ); + } + if (!isPasswordLoginSettingOn()) { + throw new LoginMethodError( + "Cannot enable 2FA while password login is disabled. Enable password login first.", + 409, + "password_login_disabled", + ); + } +} + /** * Off by default, matching 2.8: second factors only ran after password and * other local form logins. On, they also run after external methods (SSO, diff --git a/src/backend/auth/legacy-providers.ts b/src/backend/auth/legacy-providers.ts index 6d04dc1c5..77ebd1e45 100644 --- a/src/backend/auth/legacy-providers.ts +++ b/src/backend/auth/legacy-providers.ts @@ -16,12 +16,7 @@ import type { SshAuthProvider, SshConnectHost, } from "../hosts/connect/types.js"; -import { - registerLoginMethod, - registerSecondFactor, - type LoginMethod, -} from "./registry.js"; -import { totpSecondFactor } from "./legacy/totp-factor.js"; +import { registerLoginMethod, type LoginMethod } from "./registry.js"; const OPKSSH_REQUIRED_MESSAGE = "OPKSSH authentication required. Please open a Terminal connection to this host first to complete browser-based authentication. Your session will be cached for 24 hours."; @@ -328,22 +323,6 @@ const ldapLoginMethod: LoginMethod = { }, }; -// Passkeys prove the user as well as possession, so a passkey login can -// stand in for the second factor. No describe: a passkey only helps a user -// who already registered one, so it never counts for the lockout guard. -const passkeyLoginMethod: LoginMethod = { - id: "passkey", - pluginId: "core", - labelKey: "auth.loginWithPasskey", - icon: "fingerprint", - kind: "form", - verify: async (request) => { - const { verifyPasskeyLogin } = - await import("../database/routes/user-webauthn-routes.js"); - return verifyPasskeyLogin(request as never); - }, -}; - let loginRegistered = false; export function registerLegacyLoginProviders(): void { @@ -351,8 +330,6 @@ export function registerLegacyLoginProviders(): void { loginRegistered = true; registerLoginMethod(oidcLoginMethod); registerLoginMethod(ldapLoginMethod); - registerLoginMethod(passkeyLoginMethod); - registerSecondFactor(totpSecondFactor); } /** Test helper. */ diff --git a/src/backend/auth/legacy/totp-factor.ts b/src/backend/auth/legacy/totp-factor.ts deleted file mode 100644 index f7428a586..000000000 --- a/src/backend/auth/legacy/totp-factor.ts +++ /dev/null @@ -1,128 +0,0 @@ -/** - * TOTP as a second factor. Enrolment stays in the TOTP routes; this is the - * login half. Phase C moves both into the TOTP plugin. - */ - -import speakeasy from "speakeasy"; -import { AuthManager } from "../../utils/auth-manager.js"; -import { FieldCrypto } from "../../utils/field-crypto.js"; -import { LazyFieldEncryption } from "../../utils/lazy-field-encryption.js"; -import { - createCurrentUserAuthRepository, - createCurrentUserRepository, -} from "../../database/repositories/factory.js"; -import type { SecondFactor } from "../registry.js"; - -export const TOTP_FACTOR_ID = "totp"; - -/** - * Checks a backup code and burns it. Codes are stored encrypted under the - * user's data key when it was available at enrolment, so both forms are read. - */ -async function consumeBackupCode( - userId: string, - storedCodes: string | null, - code: string, - userDataKey: Buffer, -): Promise { - if (!storedCodes) return false; - const raw = LazyFieldEncryption.safeGetFieldValue( - storedCodes, - userDataKey, - userId, - "totpBackupCodes", - ); - let codes: unknown = []; - try { - codes = raw ? JSON.parse(raw) : []; - } catch { - codes = []; - } - if (!Array.isArray(codes)) return false; - const index = codes.indexOf(code); - if (index === -1) return false; - - codes.splice(index, 1); - await createCurrentUserRepository().update(userId, { - totpBackupCodes: FieldCrypto.encryptField( - JSON.stringify(codes), - userDataKey, - userId, - "totpBackupCodes", - ), - }); - return true; -} - -export const totpSecondFactor: SecondFactor = { - id: TOTP_FACTOR_ID, - pluginId: "core", - labelKey: "auth.totpFactor", - - isEnrolled: async (userId) => { - const user = await createCurrentUserRepository().findById(userId); - return !!user?.totpEnabled; - }, - - verify: async (userId, body) => { - const code = String(body.totp_code ?? body.code ?? "").trim(); - if (!code) return false; - - const user = await createCurrentUserRepository().findById(userId); - if (!user?.totpEnabled || !user.totpSecret) { - return { ok: false, error: "TOTP not enabled for this user" }; - } - - const userDataKey = AuthManager.getInstance().getUserDataKey(userId); - if (!userDataKey) { - return { - ok: false, - error: "Session expired - please log in again", - code: "SESSION_EXPIRED", - }; - } - - const secret = LazyFieldEncryption.safeGetFieldValue( - user.totpSecret, - userDataKey, - userId, - "totp_secret", - ); - if (!secret) { - // The secret was encrypted under a key a password reset replaced. - await createCurrentUserRepository().update(userId, { - totpEnabled: false, - totpSecret: null, - totpBackupCodes: null, - }); - await createCurrentUserAuthRepository().removeSecondFactor( - userId, - "core", - TOTP_FACTOR_ID, - ); - return { - ok: false, - error: - "TOTP has been disabled due to password reset. Please set up TOTP again.", - }; - } - - const verified = speakeasy.totp.verify({ - secret, - encoding: "base32", - token: code, - window: 2, - }); - if (verified) return true; - - return consumeBackupCode(userId, user.totpBackupCodes, code, userDataKey); - }, - - reset: async (userId) => { - await createCurrentUserRepository().update(userId, { - totpEnabled: false, - totpSecret: null, - totpBackupCodes: null, - }); - }, -}; diff --git a/src/backend/auth/login-pipeline.ts b/src/backend/auth/login-pipeline.ts index 869bf5a1a..0cc9d7dfd 100644 --- a/src/backend/auth/login-pipeline.ts +++ b/src/backend/auth/login-pipeline.ts @@ -186,7 +186,7 @@ async function unlockUser( const ok = identity.password ? await authManager.authenticateUser(user.id, identity.password, deviceType) - : await authManager.authenticateWebAuthnUser(user.id, deviceType); + : await authManager.unlockWithSystemKey(user.id, deviceType); if (!ok) { throw new LoginMethodError( identity.password @@ -378,12 +378,13 @@ export function consumePendingLogin(token: string): void { /** * Finishes a login that stopped for a second factor. The pending token comes - * from the body, or from the cookie a redirect method set. + * from the body, or from the cookie a redirect method set. Without a factor + * id the user's first required factor answers, for the 2.8 route. */ export async function verifySecondFactorAndRespond( req: Request, res: Response, - factorId: string, + factorId?: string, ): Promise { const lookup = await readPendingLogin(req); if (!lookup) { @@ -416,9 +417,9 @@ export async function verifySecondFactorAndRespond( code: "second_factor_unavailable", }); } - const factor = factors.required.find( - (candidate) => candidate.id === factorId, - ); + const factor = factorId + ? factors.required.find((candidate) => candidate.id === factorId) + : factors.required[0]; if (!factor) { return res .status(400) @@ -436,7 +437,7 @@ export async function verifySecondFactorAndRespond( authLogger.warn("Second factor verification failed", { operation: "totp_verify_failed", userId: user.id, - factorId, + factorId: factor.id, }); const { ipAddress, userAgent } = getRequestMeta(req); await logAudit({ @@ -444,7 +445,7 @@ export async function verifySecondFactorAndRespond( username: user.username, action: "login_second_factor_failed", resourceType: "session", - details: JSON.stringify({ factor: factorId }), + details: JSON.stringify({ factor: factor.id }), ipAddress, userAgent, success: false, diff --git a/src/backend/auth/session-issuer.ts b/src/backend/auth/session-issuer.ts index eb0426d13..bd900284c 100644 --- a/src/backend/auth/session-issuer.ts +++ b/src/backend/auth/session-issuer.ts @@ -11,7 +11,10 @@ import { loginRateLimiter } from "../utils/login-rate-limiter.js"; import { logAudit, getRequestMeta } from "../utils/audit-logger.js"; import { parseUserAgent } from "../utils/user-agent-parser.js"; import { emitInternalEvent } from "../hosts/internal-events.js"; -import { createCurrentSettingsRepository } from "../database/repositories/factory.js"; +import { + createCurrentSettingsRepository, + createCurrentUserAuthRepository, +} from "../database/repositories/factory.js"; import type { UserRecord } from "../database/repositories/user-repository.js"; const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000; @@ -139,7 +142,10 @@ export async function issueSession( username: user.username, userId: user.id, is_oidc: !!user.isOidc, - totp_enabled: !!user.totpEnabled, + // Any second factor; the name is what 2.8 clients read. + totp_enabled: await createCurrentUserAuthRepository().hasSecondFactor( + user.id, + ), ...(isNativeAppRequest(req) ? { token } : {}), }, }; diff --git a/src/backend/database/database.ts b/src/backend/database/database.ts index adbd06088..20ceeb578 100644 --- a/src/backend/database/database.ts +++ b/src/backend/database/database.ts @@ -759,10 +759,7 @@ app.post("/database/export", authenticateJWT, async (req, res) => { token_url TEXT, identifier_path TEXT, name_path TEXT, - scopes TEXT DEFAULT 'openid email profile', - totp_secret TEXT, - totp_enabled INTEGER NOT NULL DEFAULT 0, - totp_backup_codes TEXT + scopes TEXT DEFAULT 'openid email profile' ); CREATE TABLE settings ( @@ -900,8 +897,8 @@ app.post("/database/export", authenticateJWT, async (req, res) => { const userRecord = user; const insertUser = exportDb.prepare(` - INSERT INTO users (id, username, password_hash, is_admin, is_oidc, oidc_identifier, client_id, client_secret, issuer_url, authorization_url, token_url, identifier_path, name_path, scopes, totp_secret, totp_enabled, totp_backup_codes) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO users (id, username, password_hash, is_admin, is_oidc, oidc_identifier, client_id, client_secret, issuer_url, authorization_url, token_url, identifier_path, name_path, scopes) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); insertUser.run( userRecord.id, @@ -918,9 +915,6 @@ app.post("/database/export", authenticateJWT, async (req, res) => { userRecord.identifierPath || null, userRecord.namePath || null, userRecord.scopes || null, - userRecord.totpSecret || null, - userRecord.totpEnabled ? 1 : 0, - userRecord.totpBackupCodes || null, ); const sshHosts = diff --git a/src/backend/database/db/index.ts b/src/backend/database/db/index.ts index a270f3ab9..e9209ac79 100644 --- a/src/backend/database/db/index.ts +++ b/src/backend/database/db/index.ts @@ -232,9 +232,6 @@ async function initializeCompleteDatabase(): Promise { identifier_path TEXT, name_path TEXT, scopes TEXT DEFAULT 'openid email profile', - totp_secret TEXT, - totp_enabled INTEGER NOT NULL DEFAULT 0, - totp_backup_codes TEXT, registered_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, donation_modal_dismissed INTEGER NOT NULL DEFAULT 0 ); @@ -291,22 +288,6 @@ async function initializeCompleteDatabase(): Promise { FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ); - CREATE TABLE IF NOT EXISTS webauthn_credentials ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - credential_id TEXT NOT NULL UNIQUE, - public_key TEXT NOT NULL, - counter INTEGER NOT NULL DEFAULT 0, - device_type TEXT, - backed_up INTEGER NOT NULL DEFAULT 0, - transports TEXT, - user_verification TEXT NOT NULL DEFAULT 'preferred', - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - last_used_at TEXT, - FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE - ); - CREATE TABLE IF NOT EXISTS ssh_data ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, @@ -884,10 +865,6 @@ const migrateSchema = () => { addColumnIfNotExists("users", "name_path", "TEXT"); addColumnIfNotExists("users", "scopes", "TEXT"); - addColumnIfNotExists("users", "totp_secret", "TEXT"); - addColumnIfNotExists("users", "totp_enabled", "INTEGER NOT NULL DEFAULT 0"); - addColumnIfNotExists("users", "totp_backup_codes", "TEXT"); - const hadRegisteredAtColumn = (() => { try { sqlite.prepare(`SELECT "registered_at" FROM users LIMIT 1`).get(); @@ -940,24 +917,6 @@ const migrateSchema = () => { addColumnIfNotExists("sessions", "oidc_sid", "TEXT"); addColumnIfNotExists("sessions", "sso_provider_id", "INTEGER"); - sqlite.exec(` - CREATE TABLE IF NOT EXISTS webauthn_credentials ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - credential_id TEXT NOT NULL UNIQUE, - public_key TEXT NOT NULL, - counter INTEGER NOT NULL DEFAULT 0, - device_type TEXT, - backed_up INTEGER NOT NULL DEFAULT 0, - transports TEXT, - user_verification TEXT NOT NULL DEFAULT 'preferred', - created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, - last_used_at TEXT, - FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE - ) - `); - addColumnIfNotExists("ssh_data", "name", "TEXT"); addColumnIfNotExists("ssh_data", "folder", "TEXT"); addColumnIfNotExists("ssh_data", "tags", "TEXT"); diff --git a/src/backend/database/db/schema.mysql.ts b/src/backend/database/db/schema.mysql.ts index f6ba23637..1d2718991 100644 --- a/src/backend/database/db/schema.mysql.ts +++ b/src/backend/database/db/schema.mysql.ts @@ -38,12 +38,6 @@ export const users = mysqlTable("users", { namePath: text("name_path"), scopes: text().default("openid email profile"), - totpSecret: text("totp_secret"), - totpEnabled: boolean("totp_enabled") - .notNull() - .default(false), - totpBackupCodes: text("totp_backup_codes"), - registeredAt: text("registered_at").notNull().default(sql`(CURRENT_TIMESTAMP)`), donationModalDismissed: boolean("donation_modal_dismissed") .notNull() @@ -173,25 +167,6 @@ export const userSecondFactors = mysqlTable( ], ); -export const webauthnCredentials = mysqlTable("webauthn_credentials", { - id: varchar("id", { length: 255 }).primaryKey(), - userId: varchar("user_id", { length: 255 }) - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - name: varchar("name", { length: 255 }).notNull(), - credentialId: varchar("credential_id", { length: 255 }).notNull(), - publicKey: text("public_key").notNull(), - counter: int("counter").notNull().default(0), - deviceType: text("device_type"), - backedUp: boolean("backed_up").notNull().default(false), - transports: text("transports"), - userVerification: text("user_verification").notNull().default("preferred"), - createdAt: text("created_at") - .notNull() - .default(sql`(CURRENT_TIMESTAMP)`), - lastUsedAt: text("last_used_at"), -}); - export const hosts = mysqlTable( "ssh_data", { diff --git a/src/backend/database/db/schema.pg.ts b/src/backend/database/db/schema.pg.ts index 0dbc50cd8..64affd677 100644 --- a/src/backend/database/db/schema.pg.ts +++ b/src/backend/database/db/schema.pg.ts @@ -39,12 +39,6 @@ export const users = pgTable("users", { namePath: text("name_path"), scopes: text().default("openid email profile"), - totpSecret: text("totp_secret"), - totpEnabled: boolean("totp_enabled") - .notNull() - .default(false), - totpBackupCodes: text("totp_backup_codes"), - registeredAt: text("registered_at").notNull().default(sql`CURRENT_TIMESTAMP`), donationModalDismissed: boolean("donation_modal_dismissed") .notNull() @@ -174,25 +168,6 @@ export const userSecondFactors = pgTable( ], ); -export const webauthnCredentials = pgTable("webauthn_credentials", { - id: varchar("id", { length: 255 }).primaryKey(), - userId: varchar("user_id", { length: 255 }) - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - name: varchar("name", { length: 255 }).notNull(), - credentialId: varchar("credential_id", { length: 255 }).notNull(), - publicKey: text("public_key").notNull(), - counter: integer("counter").notNull().default(0), - deviceType: text("device_type"), - backedUp: boolean("backed_up").notNull().default(false), - transports: text("transports"), - userVerification: text("user_verification").notNull().default("preferred"), - createdAt: text("created_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - lastUsedAt: text("last_used_at"), -}); - export const hosts = pgTable( "ssh_data", { diff --git a/src/backend/database/db/schema.ts b/src/backend/database/db/schema.ts index e7fd00630..b63e5a374 100644 --- a/src/backend/database/db/schema.ts +++ b/src/backend/database/db/schema.ts @@ -27,12 +27,6 @@ export const users = sqliteTable("users", { namePath: text("name_path"), scopes: text().default("openid email profile"), - totpSecret: text("totp_secret"), - totpEnabled: integer("totp_enabled", { mode: "boolean" }) - .notNull() - .default(false), - totpBackupCodes: text("totp_backup_codes"), - registeredAt: text("registered_at").notNull().default(sql`CURRENT_TIMESTAMP`), donationModalDismissed: integer("donation_modal_dismissed", { mode: "boolean", @@ -164,25 +158,6 @@ export const userSecondFactors = sqliteTable( ], ); -export const webauthnCredentials = sqliteTable("webauthn_credentials", { - id: text("id").primaryKey(), - userId: text("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - name: text("name").notNull(), - credentialId: text("credential_id").notNull(), - publicKey: text("public_key").notNull(), - counter: integer("counter").notNull().default(0), - deviceType: text("device_type"), - backedUp: integer("backed_up", { mode: "boolean" }).notNull().default(false), - transports: text("transports"), - userVerification: text("user_verification").notNull().default("preferred"), - createdAt: text("created_at") - .notNull() - .default(sql`CURRENT_TIMESTAMP`), - lastUsedAt: text("last_used_at"), -}); - export const hosts = sqliteTable( "ssh_data", { diff --git a/src/backend/database/repositories/factory.ts b/src/backend/database/repositories/factory.ts index ab2313e4e..108f4a144 100644 --- a/src/backend/database/repositories/factory.ts +++ b/src/backend/database/repositories/factory.ts @@ -3,7 +3,6 @@ import { getDb, getSqlite } from "../db/index.js"; import { needsExplicitPersist, resolveDatabaseDialect } from "../db/dialect.js"; import { primeSettingsCache, readCachedSetting } from "./settings-cache.js"; import type { DatabaseContext } from "./database-context.js"; -import { WebauthnCredentialRepository } from "./webauthn-credential-repository.js"; import { NotificationChannelRepository } from "./notification-channel-repository.js"; import { ApiKeyRepository } from "./api-key-repository.js"; import { AuditLogRepository } from "./audit-log-repository.js"; @@ -128,13 +127,6 @@ export function getCurrentSettingValue(key: string): string | null { return row?.value ?? null; } -export function createCurrentWebauthnCredentialRepository(): WebauthnCredentialRepository { - return new WebauthnCredentialRepository( - createCurrentRepositoryContext(), - createCurrentRepositoryWriteHook("webauthn_credential_repository_write"), - ); -} - export function createCurrentNotificationChannelRepository(): NotificationChannelRepository { return new NotificationChannelRepository( createCurrentRepositoryContext(), diff --git a/src/backend/database/repositories/user-auth-repository.ts b/src/backend/database/repositories/user-auth-repository.ts index 2508ee4dc..6df223f67 100644 --- a/src/backend/database/repositories/user-auth-repository.ts +++ b/src/backend/database/repositories/user-auth-repository.ts @@ -113,6 +113,27 @@ export class UserAuthRepository { return this.context.drizzle.select().from(userSecondFactors); } + /** Users enrolled in at least one second factor, whichever plugin owns it. */ + async listUserIdsWithSecondFactors(): Promise> { + const rows = await this.context.drizzle + .selectDistinct({ userId: userSecondFactors.userId }) + .from(userSecondFactors); + return new Set(rows.map((row) => row.userId)); + } + + async countUsersWithSecondFactors(): Promise { + return (await this.listUserIdsWithSecondFactors()).size; + } + + async hasSecondFactor(userId: string): Promise { + const rows = await this.context.drizzle + .select({ userId: userSecondFactors.userId }) + .from(userSecondFactors) + .where(eq(userSecondFactors.userId, userId)) + .limit(1); + return rows.length > 0; + } + async recordSecondFactor( userId: string, pluginId: string, diff --git a/src/backend/database/repositories/user-repository.ts b/src/backend/database/repositories/user-repository.ts index 3c87d61e1..b341d231a 100644 --- a/src/backend/database/repositories/user-repository.ts +++ b/src/backend/database/repositories/user-repository.ts @@ -228,15 +228,6 @@ export class UserRepository { return rows.length; } - async countTotpEnabled(): Promise { - const rows = await this.context.drizzle - .select({ id: users.id }) - .from(users) - .where(eq(users.totpEnabled, true)); - - return rows.length; - } - async countAll(): Promise { const rows = await this.context.drizzle .select({ id: users.id }) diff --git a/src/backend/database/repositories/webauthn-credential-repository.ts b/src/backend/database/repositories/webauthn-credential-repository.ts deleted file mode 100644 index 5810cadea..000000000 --- a/src/backend/database/repositories/webauthn-credential-repository.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { and, eq } from "drizzle-orm"; -import { webauthnCredentials } from "../db/schema.js"; -import type { DatabaseContext } from "./database-context.js"; -import { rowsAffected } from "./mutation-result.js"; -import { insertReturning } from "./returning.js"; - -export type WebauthnCredentialRecord = typeof webauthnCredentials.$inferSelect; -export type NewWebauthnCredentialRecord = - typeof webauthnCredentials.$inferInsert; - -export interface WebauthnCredentialAuthState { - counter: number; - backedUp: boolean; - deviceType: string | null; - lastUsedAt: string; -} - -export class WebauthnCredentialRepository { - constructor( - private readonly context: DatabaseContext, - private readonly onWrite?: () => void | Promise, - ) {} - - async listByUserId(userId: string): Promise { - return this.context.drizzle - .select() - .from(webauthnCredentials) - .where(eq(webauthnCredentials.userId, userId)); - } - - async findByCredentialId( - credentialId: string, - ): Promise { - const rows = await this.context.drizzle - .select() - .from(webauthnCredentials) - .where(eq(webauthnCredentials.credentialId, credentialId)) - .limit(1); - - return rows[0] ?? null; - } - - async create( - record: NewWebauthnCredentialRecord, - ): Promise { - const rows = await insertReturning( - this.context, - webauthnCredentials, - record, - ); - - await this.afterWrite(); - return rows[0]; - } - - async updateAuthState( - id: string, - state: WebauthnCredentialAuthState, - ): Promise { - await this.context.drizzle - .update(webauthnCredentials) - .set(state) - .where(eq(webauthnCredentials.id, id)); - - await this.afterWrite(); - } - - async deleteForUser(userId: string, id: string): Promise { - const result = await this.context.drizzle - .delete(webauthnCredentials) - .where( - and( - eq(webauthnCredentials.id, id), - eq(webauthnCredentials.userId, userId), - ), - ); - - if (rowsAffected(result) > 0) { - await this.afterWrite(); - } - - return rowsAffected(result) > 0; - } - - private async afterWrite(): Promise { - await this.onWrite?.(); - } -} diff --git a/src/backend/database/routes/auth-routes.ts b/src/backend/database/routes/auth-routes.ts index 52706955b..fdbfc5973 100644 --- a/src/backend/database/routes/auth-routes.ts +++ b/src/backend/database/routes/auth-routes.ts @@ -175,6 +175,50 @@ export function registerAuthRoutes(router: Router): void { } }); + /** + * @openapi + * /users/totp/verify-login: + * post: + * summary: Finish a login with a second factor (2.8 route) + * description: Kept for clients built against 2.8, such as Termix-Mobile. Verifies the pending login against the factor named in `factor`, or the user's first enrolled factor, the same way as /users/auth/second-factor/{factorId}/verify. + * tags: + * - Auth + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * temp_token: + * type: string + * totp_code: + * type: string + * factor: + * type: string + * responses: + * 200: + * description: Login finished. + * 400: + * description: Token and code are required. + * 401: + * description: Invalid temporary token or code. + */ + router.post("/totp/verify-login", async (req, res) => { + if (!req.body?.temp_token || !req.body?.totp_code) { + return res.status(400).json({ error: "Token and code are required" }); + } + try { + await verifySecondFactorAndRespond( + req, + res, + typeof req.body.factor === "string" ? req.body.factor : undefined, + ); + } catch (error) { + sendLoginError(res, error); + } + }); + /** * @openapi * /users/auth/{methodId}/start: diff --git a/src/backend/database/routes/user-admin-routes.ts b/src/backend/database/routes/user-admin-routes.ts index 7ea85d77b..33d176bdd 100644 --- a/src/backend/database/routes/user-admin-routes.ts +++ b/src/backend/database/routes/user-admin-routes.ts @@ -112,6 +112,10 @@ export function registerUserAdminRoutes( total = pageUsers.length; } + const secondFactorUsers = requester?.isAdmin + ? await createCurrentUserAuthRepository().listUserIdsWithSecondFactors() + : new Set(); + res.json({ users: pageUsers.map((u) => ({ userId: u.id, @@ -124,7 +128,7 @@ export function registerUserAdminRoutes( ...(requester?.isAdmin ? { data_unlocked: DataCrypto.canUserAccessData(u.id), - totp_enabled: !!u.totpEnabled, + second_factor_enabled: secondFactorUsers.has(u.id), } : {}), })), @@ -473,9 +477,6 @@ export function registerUserAdminRoutes( identifierPath: "", namePath: "", scopes: "openid email profile", - totpSecret: null, - totpEnabled: false, - totpBackupCodes: null, }); try { @@ -676,100 +677,6 @@ export function registerUserAdminRoutes( } }); - /** - * @openapi - * /users/admin/totp/disable: - * post: - * summary: Disable a user's TOTP (admin only) - * description: Clears another user's TOTP secret, enabled flag and backup codes so they can log in without 2FA. - * tags: - * - Users - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * userId: - * type: string - * responses: - * 200: - * description: TOTP disabled for the user. - * 400: - * description: User ID is required or TOTP is not enabled. - * 403: - * description: Admin access required. - * 404: - * description: User not found. - * 500: - * description: Failed to disable TOTP. - */ - router.post("/admin/totp/disable", authenticateJWT, async (req, res) => { - const adminId = (req as AuthenticatedRequest).userId; - const { userId: targetUserId } = req.body; - - if (!isNonEmptyString(targetUserId)) { - return res.status(400).json({ error: "User ID is required" }); - } - - try { - const userRepository = createCurrentUserRepository(); - const adminUser = await userRepository.findById(adminId); - if (!adminUser?.isAdmin) { - return res.status(403).json({ error: "Admin access required" }); - } - - const targetUser = await userRepository.findById(targetUserId.trim()); - if (!targetUser) { - return res.status(404).json({ error: "User not found" }); - } - if (!targetUser.totpEnabled) { - return res - .status(400) - .json({ error: "TOTP is not enabled for this user" }); - } - - await userRepository.update(targetUser.id, { - totpSecret: null, - totpEnabled: false, - totpBackupCodes: null, - }); - await createCurrentUserAuthRepository().removeSecondFactor( - targetUser.id, - "core", - "totp", - ); - - try { - await DatabaseSaveTrigger.forceSave("admin_disable_totp"); - } catch (saveError) { - authLogger.error("Failed to persist TOTP disable to disk", saveError, { - operation: "admin_disable_totp_save_failed", - userId: targetUser.id, - }); - } - - const { ipAddress, userAgent } = getRequestMeta(req); - await logAudit({ - userId: adminId, - username: adminUser.username ?? adminId, - action: "admin_disable_totp", - resourceType: "user", - resourceId: targetUser.id, - resourceName: targetUser.username, - ipAddress, - userAgent, - success: true, - }); - - res.json({ message: "TOTP disabled" }); - } catch (err) { - authLogger.error("Failed to disable TOTP for user", err); - res.status(500).json({ error: "Failed to disable TOTP" }); - } - }); - /** * @openapi * /users/admin/{userId}/second-factors: diff --git a/src/backend/database/routes/user-password-reset-routes.ts b/src/backend/database/routes/user-password-reset-routes.ts index 30edcc63f..caba19dfe 100644 --- a/src/backend/database/routes/user-password-reset-routes.ts +++ b/src/backend/database/routes/user-password-reset-routes.ts @@ -84,11 +84,11 @@ export async function resetUserPassword( await deleteLegacyWraps(userId); await authManager.logoutUser(userId); - await userRepository.update(userId, { - totpEnabled: false, - totpSecret: null, - totpBackupCodes: null, - }); + // Same as 2.8, where the wipe took the TOTP secret with it: the user sets + // up their second factors again after this. + const { resetUserSecondFactors } = + await import("../../auth/second-factor-admin.js"); + await resetUserSecondFactors(userId); authLogger.warn( `Password reset completed for user: ${username}. All encrypted data has been deleted because the old key was unrecoverable.`, diff --git a/src/backend/database/routes/user-totp-routes.ts b/src/backend/database/routes/user-totp-routes.ts deleted file mode 100644 index a702dda4f..000000000 --- a/src/backend/database/routes/user-totp-routes.ts +++ /dev/null @@ -1,587 +0,0 @@ -import type { AuthenticatedRequest } from "../../../types/index.js"; -import type { Request, RequestHandler, Router } from "express"; -import bcrypt from "bcryptjs"; -import crypto from "crypto"; -import QRCode from "qrcode"; -import speakeasy from "speakeasy"; -import { AuthManager } from "../../utils/auth-manager.js"; -import { DatabaseSaveTrigger } from "../../utils/database-save-trigger.js"; -import { FieldCrypto } from "../../utils/field-crypto.js"; -import { LazyFieldEncryption } from "../../utils/lazy-field-encryption.js"; -import { authLogger } from "../../utils/logger.js"; -import { isTrustedProxyAuthEnabled } from "../../utils/trusted-proxy-auth.js"; - -import { isPasswordLoginSettingOn } from "../../auth/core-auth.js"; -import { verifySecondFactorAndRespond } from "../../auth/login-pipeline.js"; -import { TOTP_FACTOR_ID } from "../../auth/legacy/totp-factor.js"; -import { - createCurrentUserAuthRepository, - createCurrentSessionRepository, - createCurrentTrustedDeviceRepository, - createCurrentUserRepository, -} from "../repositories/factory.js"; -import type { UserRecord } from "../repositories/user-repository.js"; - -type NativeAppRequestChecker = (req: Request) => boolean; - -interface UserTotpRoutesDeps { - authenticateJWT: RequestHandler; - authManager: AuthManager; - isNativeAppRequest: NativeAppRequestChecker; -} - -const BACKUP_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; - -/** Eight characters from a CSPRNG; Math.random is not fit for secrets. */ -export function generateBackupCode(): string { - let code = ""; - for (let i = 0; i < 8; i++) { - code += BACKUP_CODE_ALPHABET[crypto.randomInt(BACKUP_CODE_ALPHABET.length)]; - } - return code; -} - -export async function verifyTotpReauth( - userRecord: UserRecord, - credential: string, - userDataKey?: Buffer | null, -): Promise { - if (userRecord.totpSecret) { - const totpSecret = userDataKey - ? LazyFieldEncryption.safeGetFieldValue( - userRecord.totpSecret, - userDataKey, - userRecord.id, - "totpSecret", - ) - : userRecord.totpSecret; - - if (totpSecret) { - const totpMatch = speakeasy.totp.verify({ - secret: totpSecret, - encoding: "base32", - token: credential, - window: 2, - }); - if (totpMatch) { - return true; - } - } - } - - const rawBackupCodes = - userDataKey && userRecord.totpBackupCodes - ? LazyFieldEncryption.safeGetFieldValue( - userRecord.totpBackupCodes, - userDataKey, - userRecord.id, - "totpBackupCodes", - ) - : userRecord.totpBackupCodes; - - let backupCodes: unknown = []; - try { - backupCodes = rawBackupCodes ? JSON.parse(rawBackupCodes) : []; - } catch { - backupCodes = []; - } - if (Array.isArray(backupCodes)) { - const backupIndex = backupCodes.indexOf(credential); - if (backupIndex !== -1) { - backupCodes.splice(backupIndex, 1); - const updatedJson = JSON.stringify(backupCodes); - const storedValue = userDataKey - ? FieldCrypto.encryptField( - updatedJson, - userDataKey, - userRecord.id, - "totpBackupCodes", - ) - : updatedJson; - await createCurrentUserRepository().update(userRecord.id, { - totpBackupCodes: storedValue, - }); - return true; - } - } - - return false; -} - -export function registerUserTotpRoutes( - router: Router, - { authenticateJWT, authManager, isNativeAppRequest }: UserTotpRoutesDeps, -): void { - /** - * @openapi - * /users/totp/setup: - * post: - * summary: Setup TOTP - * description: Initiates TOTP setup by generating a secret and QR code. - * tags: - * - Users - * responses: - * 200: - * description: TOTP setup initiated with secret and QR code. - * 400: - * description: TOTP is already enabled. - * 404: - * description: User not found. - * 500: - * description: Failed to setup TOTP. - */ - router.post("/totp/setup", authenticateJWT, async (req, res) => { - const userId = (req as AuthenticatedRequest).userId; - - try { - const userRecord = await createCurrentUserRepository().findById(userId); - if (!userRecord) { - return res.status(404).json({ error: "User not found" }); - } - - if (userRecord.totpEnabled) { - const credential = req.body?.credential; - if (!credential) { - return res.status(400).json({ - error: "A TOTP code or password is required", - }); - } - - const userDataKey = authManager.getUserDataKey(userId); - let verified = await verifyTotpReauth( - userRecord, - credential, - userDataKey, - ); - if (!verified && !userRecord.isOidc && userRecord.passwordHash) { - verified = await bcrypt.compare(credential, userRecord.passwordHash); - } - if (!verified) { - return res.status(401).json({ - error: "Incorrect password or invalid TOTP code", - }); - } - - const existingSecret = userDataKey - ? LazyFieldEncryption.safeGetFieldValue( - userRecord.totpSecret, - userDataKey, - userId, - "totpSecret", - ) - : userRecord.totpSecret; - if (!existingSecret) { - return res.status(409).json({ error: "TOTP secret is unavailable" }); - } - - const otpauthUrl = speakeasy.otpauthURL({ - secret: existingSecret, - label: `Termix (${userRecord.username})`, - encoding: "base32", - }); - authLogger.info("Additional TOTP authenticator enrollment started", { - operation: "totp_add_authenticator", - userId, - }); - return res.json({ - secret: existingSecret, - qr_code: await QRCode.toDataURL(otpauthUrl), - additional: true, - }); - } - - const secret = speakeasy.generateSecret({ - name: `Termix (${userRecord.username})`, - length: 32, - }); - - await createCurrentUserRepository().update(userId, { - totpSecret: secret.base32, - }); - - const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url || ""); - - res.json({ - secret: secret.base32, - qr_code: qrCodeUrl, - }); - } catch (err) { - authLogger.error("Failed to setup TOTP", err); - res.status(500).json({ error: "Failed to setup TOTP" }); - } - }); - - /** - * @openapi - * /users/totp/enable: - * post: - * summary: Enable TOTP - * description: Enables TOTP after verifying the initial code. - * tags: - * - Users - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * totp_code: - * type: string - * responses: - * 200: - * description: TOTP enabled successfully with backup codes. - * 400: - * description: TOTP code is required or TOTP already enabled. - * 401: - * description: Invalid TOTP code. - * 404: - * description: User not found. - * 500: - * description: Failed to enable TOTP. - */ - router.post("/totp/enable", authenticateJWT, async (req, res) => { - if (isTrustedProxyAuthEnabled()) { - return res.status(409).json({ - error: "TOTP is disabled while trusted proxy authentication is enabled", - }); - } - const userId = (req as AuthenticatedRequest).userId; - const sessionId = (req as AuthenticatedRequest).sessionId; - const { totp_code } = req.body; - - if (!totp_code) { - return res.status(400).json({ error: "TOTP code is required" }); - } - - try { - const passwordLoginAllowed = isPasswordLoginSettingOn(); - if (!passwordLoginAllowed) { - return res.status(409).json({ - error: - "Cannot enable 2FA while password login is disabled. Enable password login first.", - }); - } - - const userRecord = await createCurrentUserRepository().findById(userId); - if (!userRecord) { - return res.status(404).json({ error: "User not found" }); - } - - if (userRecord.totpEnabled) { - return res.status(400).json({ error: "TOTP is already enabled" }); - } - - if (!userRecord.totpSecret) { - return res.status(400).json({ error: "TOTP setup not initiated" }); - } - - const userDataKey = authManager.getUserDataKey(userId); - const totpSecret = userDataKey - ? LazyFieldEncryption.safeGetFieldValue( - userRecord.totpSecret, - userDataKey, - userId, - "totpSecret", - ) - : userRecord.totpSecret; - - const verified = speakeasy.totp.verify({ - secret: totpSecret, - encoding: "base32", - token: totp_code, - window: 2, - }); - - if (!verified) { - return res.status(401).json({ error: "Invalid TOTP code" }); - } - - const backupCodes = Array.from({ length: 8 }, () => generateBackupCode()); - - const backupCodesJson = JSON.stringify(backupCodes); - const storedBackupCodes = userDataKey - ? FieldCrypto.encryptField( - backupCodesJson, - userDataKey, - userId, - "totpBackupCodes", - ) - : backupCodesJson; - - await createCurrentUserRepository().update(userId, { - totpEnabled: true, - totpBackupCodes: storedBackupCodes, - }); - await createCurrentUserAuthRepository().recordSecondFactor( - userId, - "core", - TOTP_FACTOR_ID, - ); - - await createCurrentSessionRepository().revokeAllForUser( - userId, - sessionId, - ); - await createCurrentTrustedDeviceRepository().deleteByUserId(userId); - - try { - await DatabaseSaveTrigger.forceSave("totp_enable_explicit_save"); - } catch (saveError) { - authLogger.error( - "Failed to persist TOTP enablement to disk", - saveError, - { - operation: "totp_enable_db_save_failed", - userId, - }, - ); - } - - res.json({ - message: "TOTP enabled successfully", - backup_codes: backupCodes, - }); - } catch (err) { - authLogger.error("Failed to enable TOTP", err); - res.status(500).json({ error: "Failed to enable TOTP" }); - } - }); - - /** - * @openapi - * /users/totp/disable: - * post: - * summary: Disable TOTP - * description: Disables TOTP for a user. - * tags: - * - Users - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * password: - * type: string - * totp_code: - * type: string - * responses: - * 200: - * description: TOTP disabled successfully. - * 400: - * description: Password or TOTP code is required. - * 401: - * description: Incorrect password or invalid TOTP code. - * 404: - * description: User not found. - * 500: - * description: Failed to disable TOTP. - */ - router.post("/totp/disable", authenticateJWT, async (req, res) => { - const userId = (req as AuthenticatedRequest).userId; - const { password, totp_code } = req.body; - try { - const userRecord = await createCurrentUserRepository().findById(userId); - if (!userRecord) { - return res.status(404).json({ error: "User not found" }); - } - - // One re-authentication value, whichever kind it is. The dialog offers a - // single field -- "Enter TOTP code or password" -- so it arrives in - // whichever of the two body fields the caller happened to use. - const credential = totp_code || password; - if (!credential) { - return res.status(400).json({ - error: userRecord.isOidc - ? "A TOTP code is required" - : "A TOTP code or password is required", - }); - } - - if (!userRecord.totpEnabled) { - return res.status(400).json({ error: "TOTP is not enabled" }); - } - - const userDataKey = authManager.getUserDataKey(userId); - // TOTP code or backup code first; verifyTotpReauth deliberately refuses - // the account password, so that stays a separate comparison here. - let verified = await verifyTotpReauth( - userRecord, - credential, - userDataKey, - ); - - if (!verified && !userRecord.isOidc && userRecord.passwordHash) { - verified = await bcrypt.compare(credential, userRecord.passwordHash); - } - - if (!verified) { - return res - .status(401) - .json({ error: "Incorrect password or invalid TOTP code" }); - } - - await createCurrentUserRepository().update(userId, { - totpEnabled: false, - totpSecret: null, - totpBackupCodes: null, - }); - await createCurrentUserAuthRepository().removeSecondFactor( - userId, - "core", - TOTP_FACTOR_ID, - ); - authLogger.info("Two-factor authentication disabled", { - operation: "totp_disable", - userId, - }); - - res.json({ message: "TOTP disabled successfully" }); - } catch (err) { - authLogger.error("Failed to disable TOTP", err); - res.status(500).json({ error: "Failed to disable TOTP" }); - } - }); - - /** - * @openapi - * /users/totp/backup-codes: - * post: - * summary: Generate new backup codes - * description: Generates new TOTP backup codes. - * tags: - * - Users - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * password: - * type: string - * totp_code: - * type: string - * responses: - * 200: - * description: New backup codes generated. - * 400: - * description: Password or TOTP code is required. - * 401: - * description: Incorrect password or invalid TOTP code. - * 404: - * description: User not found. - * 500: - * description: Failed to generate backup codes. - */ - router.post("/totp/backup-codes", authenticateJWT, async (req, res) => { - const userId = (req as AuthenticatedRequest).userId; - const { password, totp_code } = req.body; - try { - const userRecord = await createCurrentUserRepository().findById(userId); - if (!userRecord) { - return res.status(404).json({ error: "User not found" }); - } - - if (!totp_code || (!userRecord.isOidc && !password)) { - return res.status(400).json({ - error: userRecord.isOidc - ? "A TOTP code is required" - : "Both password and TOTP code are required", - }); - } - - if ( - !userRecord.isOidc && - (!userRecord.passwordHash || - !(await bcrypt.compare(password, userRecord.passwordHash))) - ) { - return res.status(401).json({ error: "Incorrect password" }); - } - - if (!userRecord.totpEnabled) { - return res.status(400).json({ error: "TOTP is not enabled" }); - } - - const userDataKey = authManager.getUserDataKey(userId); - const verified = await verifyTotpReauth( - userRecord, - totp_code, - userDataKey, - ); - if (!verified) { - return res - .status(401) - .json({ error: "Incorrect password or invalid TOTP code" }); - } - - const backupCodes = Array.from({ length: 8 }, () => generateBackupCode()); - - const backupCodesJson = JSON.stringify(backupCodes); - const storedBackupCodes = userDataKey - ? FieldCrypto.encryptField( - backupCodesJson, - userDataKey, - userId, - "totpBackupCodes", - ) - : backupCodesJson; - - await createCurrentUserRepository().update(userId, { - totpBackupCodes: storedBackupCodes, - }); - - res.json({ backup_codes: backupCodes }); - } catch (err) { - authLogger.error("Failed to generate backup codes", err); - res.status(500).json({ error: "Failed to generate backup codes" }); - } - }); - - /** - * @openapi - * /users/totp/verify-login: - * post: - * summary: Verify TOTP during login - * description: Verifies the TOTP code during login. - * tags: - * - Users - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * temp_token: - * type: string - * totp_code: - * type: string - * responses: - * 200: - * description: TOTP verification successful. - * 400: - * description: Token and TOTP code are required. - * 401: - * description: Invalid temporary token or TOTP code. - * 404: - * description: User not found. - * 500: - * description: TOTP verification failed. - */ - router.post("/totp/verify-login", async (req, res) => { - if (!req.body?.temp_token || !req.body?.totp_code) { - return res - .status(400) - .json({ error: "Token and TOTP code are required" }); - } - try { - await verifySecondFactorAndRespond(req, res, TOTP_FACTOR_ID); - } catch (err) { - authLogger.error("TOTP verification failed", err); - return res.status(500).json({ error: "TOTP verification failed" }); - } - }); -} diff --git a/src/backend/database/routes/user-webauthn-routes.ts b/src/backend/database/routes/user-webauthn-routes.ts deleted file mode 100644 index 5720413f6..000000000 --- a/src/backend/database/routes/user-webauthn-routes.ts +++ /dev/null @@ -1,537 +0,0 @@ -import { getErrorMessage } from "../../utils/error-message.js"; -import type { Request, RequestHandler, Router } from "express"; -import type { - AuthenticationResponseJSON, - RegistrationResponseJSON, - AuthenticatorTransportFuture, - Base64URLString, - WebAuthnCredential, -} from "@simplewebauthn/server"; -import { - generateAuthenticationOptions, - generateRegistrationOptions, - verifyAuthenticationResponse, - verifyRegistrationResponse, -} from "@simplewebauthn/server"; -import { nanoid } from "nanoid"; -import type { AuthenticatedRequest } from "../../../types/index.js"; -import { AuthManager } from "../../utils/auth-manager.js"; -import { authLogger } from "../../utils/logger.js"; - -import { - createCurrentUserRepository, - createCurrentWebauthnCredentialRepository, -} from "../repositories/factory.js"; -import type { WebauthnCredentialRecord } from "../repositories/webauthn-credential-repository.js"; -import { respondWithLogin, sendLoginError } from "../../auth/login-pipeline.js"; -import { LoginMethodError, type VerifiedIdentity } from "../../auth/types.js"; - -type UserVerification = "discouraged" | "preferred" | "required"; -type NativeAppRequestChecker = (req: Request) => boolean; - -interface WebAuthnRoutesDeps { - authenticateJWT: RequestHandler; - authManager: AuthManager; - isNativeAppRequest: NativeAppRequestChecker; -} - -interface ChallengeRecord { - challenge: string; - userId?: string; - rpID: string; - origin: string; - userVerification: UserVerification; - createdAt: number; -} - -const challengeTtlMs = 5 * 60 * 1000; -const registrationChallenges = new Map(); -const authenticationChallenges = new Map(); - -function normalizeUserVerification(value: unknown): UserVerification { - return value === "discouraged" || value === "required" ? value : "preferred"; -} - -function getRequestOrigin(req: Request): string { - const origin = req.get("origin"); - if (origin) return origin; - - const proto = req.get("x-forwarded-proto") || req.protocol || "http"; - const host = req.get("x-forwarded-host") || req.get("host") || "localhost"; - return `${proto.split(",")[0]}://${host.split(",")[0]}`; -} - -function getRpID(origin: string): string { - return new URL(origin).hostname; -} - -function pruneChallenges(map: Map): void { - const now = Date.now(); - for (const [id, record] of map) { - if (now - record.createdAt > challengeTtlMs) { - map.delete(id); - } - } -} - -function putChallenge( - map: Map, - record: Omit, -): string { - pruneChallenges(map); - const challengeId = nanoid(); - map.set(challengeId, { ...record, createdAt: Date.now() }); - return challengeId; -} - -function takeChallenge( - map: Map, - challengeId: unknown, -): ChallengeRecord | null { - if (typeof challengeId !== "string") return null; - pruneChallenges(map); - const record = map.get(challengeId); - if (!record) return null; - map.delete(challengeId); - return record; -} - -function toBase64Url(value: Uint8Array): string { - return Buffer.from(value).toString("base64url"); -} - -function fromBase64Url(value: string): Uint8Array { - return Uint8Array.from(Buffer.from(value, "base64url")); -} - -function parseTransports(value: string | null): AuthenticatorTransportFuture[] { - if (!value) return []; - try { - const parsed = JSON.parse(value); - return Array.isArray(parsed) ? parsed : []; - } catch { - return []; - } -} - -function getCredentialForVerification( - credential: WebauthnCredentialRecord, -): WebAuthnCredential { - return { - id: credential.credentialId as Base64URLString, - publicKey: fromBase64Url( - credential.publicKey, - ) as WebAuthnCredential["publicKey"], - counter: credential.counter, - transports: parseTransports(credential.transports), - }; -} - -/** - * Checks a passkey assertion and says who signed in. A passkey that verified - * the user (PIN or biometric) counts as the second factor too. - */ -export async function verifyPasskeyLogin( - req: Request, -): Promise { - const challenge = takeChallenge( - authenticationChallenges, - req.body?.challengeId, - ); - if (!challenge) { - throw new LoginMethodError("Authentication challenge expired", 400); - } - - const response = req.body?.response as AuthenticationResponseJSON | undefined; - if (!response?.id) { - throw new LoginMethodError("Invalid passkey response", 400); - } - - const credential = - await createCurrentWebauthnCredentialRepository().findByCredentialId( - response.id, - ); - if (!credential) { - throw new LoginMethodError("Passkey not recognized", 401); - } - if (challenge.userId && challenge.userId !== credential.userId) { - throw new LoginMethodError("Passkey not recognized", 401); - } - - let verification: Awaited>; - try { - verification = await verifyAuthenticationResponse({ - response, - expectedChallenge: challenge.challenge, - expectedOrigin: challenge.origin, - expectedRPID: challenge.rpID, - credential: getCredentialForVerification(credential), - requireUserVerification: challenge.userVerification === "required", - advancedFIDOConfig: { userVerification: challenge.userVerification }, - }); - } catch (error) { - authLogger.warn("WebAuthn authentication failed", { - operation: "webauthn_auth_verify", - credentialId: credential.id, - userId: credential.userId, - error: getErrorMessage(error, "Unknown"), - }); - throw new LoginMethodError("Passkey authentication failed", 401); - } - - if (!verification.verified) { - throw new LoginMethodError("Passkey authentication failed", 401); - } - - await createCurrentWebauthnCredentialRepository().updateAuthState( - credential.id, - { - counter: verification.authenticationInfo.newCounter, - backedUp: verification.authenticationInfo.credentialBackedUp, - deviceType: verification.authenticationInfo.credentialDeviceType, - lastUsedAt: new Date().toISOString(), - }, - ); - - return { - kind: "user", - userId: credential.userId, - mfaSatisfied: verification.authenticationInfo.userVerified === true, - rememberMe: !!req.body?.rememberMe, - unlockError: - "Passkey cannot unlock this account. Log in with password and register the passkey again.", - }; -} - -export function registerUserWebAuthnRoutes( - router: Router, - { authenticateJWT, authManager, isNativeAppRequest }: WebAuthnRoutesDeps, -): void { - /** - * @openapi - * /users/webauthn/credentials: - * get: - * summary: List passkeys - * description: Lists the authenticated user's registered passkeys. - * tags: - * - WebAuthn - * responses: - * 200: - * description: List of passkeys. - * 401: - * description: Authentication required. - */ - router.get("/webauthn/credentials", authenticateJWT, async (req, res) => { - const userId = (req as AuthenticatedRequest).userId; - if (!userId) { - return res.status(401).json({ error: "Authentication required" }); - } - - const credentials = - await createCurrentWebauthnCredentialRepository().listByUserId(userId); - - res.json({ - credentials: credentials.map((credential) => ({ - id: credential.id, - name: credential.name, - deviceType: credential.deviceType, - backedUp: credential.backedUp, - transports: parseTransports(credential.transports), - userVerification: credential.userVerification, - createdAt: credential.createdAt, - lastUsedAt: credential.lastUsedAt, - })), - }); - }); - - /** - * @openapi - * /users/webauthn/register/options: - * post: - * summary: Start passkey registration - * description: Generates WebAuthn registration options for the authenticated user. - * tags: - * - WebAuthn - * responses: - * 200: - * description: Registration options and challenge id. - * 401: - * description: Authentication required. - * 404: - * description: User not found. - */ - router.post( - "/webauthn/register/options", - authenticateJWT, - async (req, res) => { - const userId = (req as AuthenticatedRequest).userId; - if (!userId) { - return res.status(401).json({ error: "Authentication required" }); - } - - if (!authManager.getUserDataKey(userId)) { - return res.status(401).json({ - error: "User data is locked. Log in again before adding a passkey.", - }); - } - - const user = await createCurrentUserRepository().findById(userId); - if (!user) { - return res.status(404).json({ error: "User not found" }); - } - - const existing = - await createCurrentWebauthnCredentialRepository().listByUserId(userId); - - const origin = getRequestOrigin(req); - const rpID = getRpID(origin); - const userVerification = normalizeUserVerification( - req.body?.userVerification, - ); - - const options = await generateRegistrationOptions({ - rpName: "Termix", - rpID, - userID: Buffer.from(userId, "utf8"), - userName: user.username, - userDisplayName: user.username, - attestationType: "none", - excludeCredentials: existing.map((credential) => ({ - id: credential.credentialId as Base64URLString, - transports: parseTransports(credential.transports), - })), - authenticatorSelection: { - residentKey: "required", - userVerification, - }, - }); - - const challengeId = putChallenge(registrationChallenges, { - challenge: options.challenge, - userId, - rpID, - origin, - userVerification, - }); - - res.json({ options, challengeId }); - }, - ); - - /** - * @openapi - * /users/webauthn/register/verify: - * post: - * summary: Finish passkey registration - * description: Verifies the WebAuthn registration response and stores the passkey. - * tags: - * - WebAuthn - * responses: - * 200: - * description: Passkey registered. - * 400: - * description: Registration failed or challenge expired. - * 401: - * description: Authentication required. - */ - router.post( - "/webauthn/register/verify", - authenticateJWT, - async (req, res) => { - const userId = (req as AuthenticatedRequest).userId; - if (!userId) { - return res.status(401).json({ error: "Authentication required" }); - } - - const challenge = takeChallenge( - registrationChallenges, - req.body?.challengeId, - ); - if (!challenge || challenge.userId !== userId) { - return res - .status(400) - .json({ error: "Registration challenge expired" }); - } - - try { - const verification = await verifyRegistrationResponse({ - response: req.body?.response as RegistrationResponseJSON, - expectedChallenge: challenge.challenge, - expectedOrigin: challenge.origin, - expectedRPID: challenge.rpID, - requireUserVerification: challenge.userVerification === "required", - }); - - if (!verification.verified) { - return res.status(400).json({ error: "Passkey registration failed" }); - } - - const { credential, credentialDeviceType, credentialBackedUp } = - verification.registrationInfo; - const transports = - (req.body?.response as RegistrationResponseJSON | undefined)?.response - ?.transports ?? []; - - const name = - typeof req.body?.name === "string" && req.body.name.trim() - ? req.body.name.trim().slice(0, 80) - : "Passkey"; - - await createCurrentWebauthnCredentialRepository().create({ - id: nanoid(), - userId, - name, - credentialId: credential.id, - publicKey: toBase64Url(credential.publicKey), - counter: credential.counter, - deviceType: credentialDeviceType, - backedUp: credentialBackedUp, - transports: JSON.stringify(transports), - userVerification: challenge.userVerification, - createdAt: new Date().toISOString(), - }); - - res.json({ success: true }); - } catch (error) { - authLogger.warn("WebAuthn registration failed", { - operation: "webauthn_register_verify", - userId, - error: getErrorMessage(error, "Unknown"), - }); - res.status(400).json({ error: "Passkey registration failed" }); - } - }, - ); - - /** - * @openapi - * /users/webauthn/authenticate/options: - * post: - * summary: Start passkey login - * description: Generates WebAuthn authentication options, optionally scoped to a username. - * tags: - * - WebAuthn - * responses: - * 200: - * description: Authentication options and challenge id. - * 404: - * description: No passkeys found for the user. - */ - router.post("/webauthn/authenticate/options", async (req, res) => { - const origin = getRequestOrigin(req); - const rpID = getRpID(origin); - const userVerification = normalizeUserVerification( - req.body?.userVerification, - ); - const username = - typeof req.body?.username === "string" ? req.body.username.trim() : ""; - - let userId: string | undefined; - let allowCredentials: - | { id: Base64URLString; transports?: AuthenticatorTransportFuture[] }[] - | undefined; - - if (username) { - const user = await createCurrentUserRepository().findByUsername(username); - if (!user) { - return res.status(404).json({ error: "No passkeys found" }); - } - - userId = user.id; - const credentials = - await createCurrentWebauthnCredentialRepository().listByUserId(userId); - - if (!credentials.length) { - return res.status(404).json({ error: "No passkeys found" }); - } - - allowCredentials = credentials.map((credential) => ({ - id: credential.credentialId as Base64URLString, - transports: parseTransports(credential.transports), - })); - } - - const options = await generateAuthenticationOptions({ - rpID, - allowCredentials, - userVerification, - }); - - const challengeId = putChallenge(authenticationChallenges, { - challenge: options.challenge, - userId, - rpID, - origin, - userVerification, - }); - - res.json({ options, challengeId }); - }); - - /** - * @openapi - * /users/webauthn/authenticate/verify: - * post: - * summary: Finish passkey login - * description: Verifies the WebAuthn assertion and issues a session token. Verified WebAuthn user verification satisfies 2FA; possession-only keys retain the configured TOTP challenge. - * tags: - * - WebAuthn - * responses: - * 200: - * description: Login succeeded. - * 400: - * description: Challenge expired or invalid response. - * 401: - * description: Passkey not recognized or authentication failed. - */ - router.post("/webauthn/authenticate/verify", async (req, res) => { - try { - const identity = await verifyPasskeyLogin(req); - await respondWithLogin(req, res, identity, { - methodId: "passkey", - rememberMe: !!req.body?.rememberMe, - }); - } catch (error) { - sendLoginError(res, error); - } - }); - - /** - * @openapi - * /users/webauthn/credentials/{credentialId}: - * delete: - * summary: Delete a passkey - * description: Removes one of the authenticated user's passkeys. - * tags: - * - WebAuthn - * parameters: - * - in: path - * name: credentialId - * required: true - * schema: { type: string } - * responses: - * 200: - * description: Passkey deleted. - * 401: - * description: Authentication required. - */ - router.delete( - "/webauthn/credentials/:credentialId", - authenticateJWT, - async (req, res) => { - const userId = (req as AuthenticatedRequest).userId; - if (!userId) { - return res.status(401).json({ error: "Authentication required" }); - } - - const credentialId = String(req.params.credentialId); - - await createCurrentWebauthnCredentialRepository().deleteForUser( - userId, - credentialId, - ); - - res.json({ success: true }); - }, - ); -} diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index c07cf3c46..7c19af212 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -27,8 +27,6 @@ import { registerUserApiKeyRoutes } from "./user-api-key-routes.js"; import { registerBrandingRoutes } from "./branding-routes.js"; import { registerUserSettingsRoutes } from "./user-settings-routes.js"; import { registerAcmeSSLRoutes } from "./acme-ssl-routes.js"; -import { registerUserTotpRoutes } from "./user-totp-routes.js"; -import { registerUserWebAuthnRoutes } from "./user-webauthn-routes.js"; import { registerUserSessionRoutes } from "./user-session-routes.js"; import { registerUserOidcAccountRoutes } from "./user-oidc-account-routes.js"; import { registerUserPasswordResetRoutes } from "./user-password-reset-routes.js"; @@ -43,6 +41,7 @@ import { getCurrentSettingValue, createCurrentRoleRepository, createCurrentSsoProviderRepository, + createCurrentUserAuthRepository, createCurrentUserRepository, } from "../repositories/factory.js"; import type { UserRecord } from "../repositories/user-repository.js"; @@ -215,9 +214,6 @@ router.post("/create", async (req, res) => { identifierPath: "", namePath: "", scopes: "openid email profile", - totpSecret: null, - totpEnabled: false, - totpBackupCodes: null, }); try { @@ -750,9 +746,13 @@ router.post("/proxy-login", async (req, res) => { if (!userRecord) { return res.status(403).json({ error: "Proxy user must already exist" }); } - if (userRecord.isOidc || userRecord.totpEnabled) { + if ( + userRecord.isOidc || + (await createCurrentUserAuthRepository().hasSecondFactor(userRecord.id)) + ) { return res.status(409).json({ - error: "Proxy authentication cannot be used with OIDC or TOTP users", + error: + "Proxy authentication cannot be used with OIDC or second factor users", }); } @@ -795,10 +795,7 @@ router.post("/proxy-login", async (req, res) => { const deviceInfo = parseUserAgent(req); if ( - !(await authManager.authenticateWebAuthnUser( - userRecord.id, - deviceInfo.type, - )) + !(await authManager.unlockWithSystemKey(userRecord.id, deviceInfo.type)) ) { return res .status(409) @@ -1057,7 +1054,10 @@ router.get("/me", authenticateJWT, async (req: Request, res: Response) => { is_admin: !!user.isAdmin, is_oidc: !!user.isOidc, is_dual_auth: isDualAuth, - totp_enabled: !!user.totpEnabled, + // Any second factor; the name is what 2.8 clients read. + totp_enabled: await createCurrentUserAuthRepository().hasSecondFactor( + user.id, + ), show_donation_modal: showDonationModal, }); } catch (err) { @@ -1702,9 +1702,9 @@ router.patch("/password-login-allowed", authenticateJWT, async (req, res) => { return res.status(400).json({ error: "Invalid value for allowed" }); } if (!allowed) { - const totpEnabledCount = - await createCurrentUserRepository().countTotpEnabled(); - if (totpEnabledCount > 0) { + const secondFactorUsers = + await createCurrentUserAuthRepository().countUsersWithSecondFactors(); + if (secondFactorUsers > 0) { return res.status(409).json({ error: "Cannot disable password login while 2FA is enabled for one or more users. Disable 2FA first.", @@ -1978,18 +1978,6 @@ router.post("/change-password", authenticateJWT, async (req, res) => { registerUserAdminRoutes(router, authenticateJWT); -registerUserTotpRoutes(router, { - authenticateJWT, - authManager, - isNativeAppRequest, -}); - -registerUserWebAuthnRoutes(router, { - authenticateJWT, - authManager, - isNativeAppRequest, -}); - /** * @openapi * /users/delete-user: diff --git a/src/backend/plugins/actor.ts b/src/backend/plugins/actor.ts index 517f2161b..ff5cd88a2 100644 --- a/src/backend/plugins/actor.ts +++ b/src/backend/plugins/actor.ts @@ -24,6 +24,8 @@ interface ActorStore { userId: string; /** Where the identity came from, for the audit line. */ source: "request" | "asUser" | "service"; + /** The session a request came in on, so core can spare it when revoking. */ + sessionId?: string; } const storage = new AsyncLocalStorage(); @@ -32,8 +34,13 @@ export function runAsActor( userId: string, source: ActorStore["source"], fn: () => T, + sessionId?: string, ): T { - return storage.run({ userId, source }, fn); + return storage.run({ userId, source, sessionId }, fn); +} + +export function getActorSessionId(): string | undefined { + return storage.getStore()?.sessionId; } export function getActor(): string | undefined { diff --git a/src/backend/plugins/ctx-ssh-auth.ts b/src/backend/plugins/ctx-ssh-auth.ts index a0a1c9d80..5d5a55e70 100644 --- a/src/backend/plugins/ctx-ssh-auth.ts +++ b/src/backend/plugins/ctx-ssh-auth.ts @@ -18,7 +18,7 @@ import { PluginSshInteractionError, } from "@termix/plugin-sdk/backend"; import { assertCapability } from "./permissions.js"; -import { getActor } from "./actor.js"; +import { getActor, getActorSessionId } from "./actor.js"; import type { DisposableBag } from "./disposables.js"; import { getSshAuthProvider, @@ -511,13 +511,26 @@ export function createPluginAuth({ manifest, bag, audit }: Deps): PluginAuth { recordEnrollment: async (userId, factorId) => { requireDeclared(contributes.secondFactors, factorId, "secondFactors"); await granted(); - const { createCurrentUserAuthRepository } = - await import("../database/repositories/factory.js"); + const { assertSecondFactorEnrollmentAllowed } = + await import("../auth/core-auth.js"); + assertSecondFactorEnrollmentAllowed(); + const { + createCurrentSessionRepository, + createCurrentTrustedDeviceRepository, + createCurrentUserAuthRepository, + } = await import("../database/repositories/factory.js"); await createCurrentUserAuthRepository().recordSecondFactor( userId, pluginId, factorId, ); + // A new factor signs the user out everywhere else and forgets trusted + // devices, so nothing that skipped it before keeps skipping it. + await createCurrentSessionRepository().revokeAllForUser( + userId, + getActor() === userId ? getActorSessionId() : undefined, + ); + await createCurrentTrustedDeviceRepository().deleteByUserId(userId); await audit("auth_factor_enrolled", `${factorId} for ${userId}`, { success: true, }); diff --git a/src/backend/plugins/ctx.ts b/src/backend/plugins/ctx.ts index 87514a621..21f91ad4f 100644 --- a/src/backend/plugins/ctx.ts +++ b/src/backend/plugins/ctx.ts @@ -600,6 +600,26 @@ export function createPluginContext( { action: "secret_delete", details: () => "deleted a secret" }, ), + seal: async (value) => { + await assertCapability(pluginId, "secrets:own", declared); + const { encryptSystemSecret } = + await import("../utils/system-secret-crypto.js"); + return encryptSystemSecret(value); + }, + + unseal: async (sealed) => { + await assertCapability(pluginId, "secrets:own", declared); + const { decryptSystemSecret, isSystemEncrypted } = + await import("../utils/system-secret-crypto.js"); + // Only values seal() wrote: a plain string must not pass as sealed. + if (!sealed || !isSystemEncrypted(sealed)) return null; + try { + return await decryptSystemSecret(sealed); + } catch { + return null; + } + }, + offer: (key, resolve) => { const entry = manifest.providesSecret?.find( (candidate) => candidate.key === key, diff --git a/src/backend/plugins/http.ts b/src/backend/plugins/http.ts index 84fa75818..cec8f117c 100644 --- a/src/backend/plugins/http.ts +++ b/src/backend/plugins/http.ts @@ -204,12 +204,15 @@ export function createPluginRouter({ // After auth, so the actor is the user core authenticated rather than // whatever the request claimed. outer.use((req: Request, _res: Response, next: NextFunction) => { - const userId = (req as Request & { userId?: string }).userId; + const { userId, sessionId } = req as Request & { + userId?: string; + sessionId?: string; + }; if (!userId) { next(); return; } - runAsActor(userId, "request", () => next()); + runAsActor(userId, "request", () => next(), sessionId); }); outer.use(inner); diff --git a/src/backend/starter.ts b/src/backend/starter.ts index 46f82c089..cc08cb7fe 100644 --- a/src/backend/starter.ts +++ b/src/backend/starter.ts @@ -70,9 +70,6 @@ async function provisionLocalDesktopUserIfNeeded(): Promise { identifierPath: "", namePath: "", scopes: "openid email profile", - totpSecret: null, - totpEnabled: false, - totpBackupCodes: null, }); try { @@ -203,18 +200,21 @@ async function provisionLocalDesktopUserIfNeeded(): Promise { const { createCurrentSettingsRepository, createCurrentSsoProviderRepository, + createCurrentUserAuthRepository, createCurrentUserRepository, } = await import("./database/repositories/factory.js"); - const [legacyOidc, providers, users] = await Promise.all([ - createCurrentSettingsRepository().get("oidc_config"), - createCurrentSsoProviderRepository().listEnabled(), - createCurrentUserRepository().listAll(), - ]); + const [legacyOidc, providers, users, secondFactorUsers] = + await Promise.all([ + createCurrentSettingsRepository().get("oidc_config"), + createCurrentSsoProviderRepository().listEnabled(), + createCurrentUserRepository().listAll(), + createCurrentUserAuthRepository().listUserIdsWithSecondFactors(), + ]); const conflictingProvider = providers.some((provider) => ["oidc", "github", "google"].includes(provider.type), ); const conflictingUser = users.some( - (user) => user.isOidc || user.totpEnabled, + (user) => user.isOidc || secondFactorUsers.has(user.id), ); if ( legacyOidc || @@ -223,7 +223,7 @@ async function provisionLocalDesktopUserIfNeeded(): Promise { conflictingUser ) { throw new Error( - "Trusted proxy authentication cannot start while OIDC or TOTP is enabled", + "Trusted proxy authentication cannot start while OIDC or a second factor is enabled", ); } systemLogger.info("Trusted proxy authentication enabled", { @@ -409,6 +409,10 @@ async function provisionLocalDesktopUserIfNeeded(): Promise { const { runSecretSourcesTokenMigration } = await import("./utils/crypto-migration/secret-sources-token-migration.js"); await runSecretSourcesTokenMigration(); + + const { runTotpMigration } = + await import("./utils/crypto-migration/totp-migration.js"); + await runTotpMigration(); } catch (error) { systemLogger.warn("Plugin runtime failed to initialize", { operation: "plugin_init", diff --git a/src/backend/tests/auth/auth-test-helpers.ts b/src/backend/tests/auth/auth-test-helpers.ts index cad0aad53..6f3b1037d 100644 --- a/src/backend/tests/auth/auth-test-helpers.ts +++ b/src/backend/tests/auth/auth-test-helpers.ts @@ -8,9 +8,6 @@ export interface FakeUser { isOidc: boolean; oidcIdentifier?: string | null; ssoProviderId?: number | null; - totpEnabled?: boolean; - totpSecret?: string | null; - totpBackupCodes?: string | null; } /** In-memory stand-ins for the repositories the login pipeline touches. */ @@ -99,6 +96,12 @@ export function fakeFactory(state: AuthState) { }, listSecondFactors: async (userId: string) => state.factors.filter((row) => row.userId === userId), + hasSecondFactor: async (userId: string) => + state.factors.some((row) => row.userId === userId), + listUserIdsWithSecondFactors: async () => + new Set(state.factors.map((row) => row.userId)), + countUsersWithSecondFactors: async () => + new Set(state.factors.map((row) => row.userId)).size, recordSecondFactor: async ( userId: string, pluginId: string, @@ -199,7 +202,7 @@ export function fakeAuthManager(state: AuthState) { }), authenticateUser: vi.fn(async () => true), authenticateOIDCUser: vi.fn(async () => true), - authenticateWebAuthnUser: vi.fn(async () => true), + unlockWithSystemKey: vi.fn(async () => true), registerOIDCUser: vi.fn(async () => {}), isTrustedDevice: vi.fn(async (userId: string, fingerprint: string) => state.trusted.has(`${userId}:${fingerprint}`), diff --git a/src/backend/tests/auth/legacy-methods.test.ts b/src/backend/tests/auth/legacy-methods.test.ts index 5bb77ba00..61499560f 100644 --- a/src/backend/tests/auth/legacy-methods.test.ts +++ b/src/backend/tests/auth/legacy-methods.test.ts @@ -358,7 +358,7 @@ describe("LDAP through the pipeline", () => { ).rejects.toMatchObject({ status: 401 }); }); - it("skips TOTP for an LDAP user who enrolled when the external-login setting is off", async () => { + it("skips second factors for an LDAP user who enrolled when the external-login setting is off", async () => { h.providerConfig = providerConfig; h.state.users.set("u-bob", { id: "u-bob", @@ -367,8 +367,11 @@ describe("LDAP through the pipeline", () => { isAdmin: false, isOidc: true, oidcIdentifier: "ldap:4:bob", - totpEnabled: true, - totpSecret: "JBSWY3DPEHPK3PXP", + }); + h.state.factors.push({ + userId: "u-bob", + pluginId: "fixture", + factorId: "pin", }); const identity = await verifyLdapLogin({ body: { providerId: 4, username: "bob", password: "hunter2" }, @@ -381,7 +384,7 @@ describe("LDAP through the pipeline", () => { expect(result.kind).toBe("session"); }); - it("runs TOTP for an LDAP user who enrolled when the external-login setting is on", async () => { + it("runs second factors for an LDAP user who enrolled when the external-login setting is on", async () => { h.providerConfig = providerConfig; h.state.settings.set("second_factor_after_external_login", "true"); h.state.users.set("u-bob", { @@ -391,17 +394,32 @@ describe("LDAP through the pipeline", () => { isAdmin: false, isOidc: true, oidcIdentifier: "ldap:4:bob", - totpEnabled: true, - totpSecret: "JBSWY3DPEHPK3PXP", + }); + h.state.factors.push({ + userId: "u-bob", + pluginId: "fixture", + factorId: "pin", }); const identity = await verifyLdapLogin({ body: { providerId: 4, username: "bob", password: "hunter2" }, ip: "10.0.0.3", }); - const result = await runLogin(fakeRequest() as never, identity, { - methodId: "ldap", - rememberMe: false, + const { registerSecondFactor } = await import("../../auth/registry.js"); + const dispose = registerSecondFactor({ + id: "pin", + pluginId: "fixture", + labelKey: "pin", + isEnrolled: async () => false, + verify: async () => true, }); - expect(result.kind).toBe("second-factor"); + try { + const result = await runLogin(fakeRequest() as never, identity, { + methodId: "ldap", + rememberMe: false, + }); + expect(result.kind).toBe("second-factor"); + } finally { + dispose(); + } }); }); diff --git a/src/backend/tests/auth/login-pipeline.test.ts b/src/backend/tests/auth/login-pipeline.test.ts index e0e661ad1..9cb16d453 100644 --- a/src/backend/tests/auth/login-pipeline.test.ts +++ b/src/backend/tests/auth/login-pipeline.test.ts @@ -1,6 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import bcrypt from "bcryptjs"; -import speakeasy from "speakeasy"; import { createAuthState, fakeAuthManager, @@ -98,7 +97,19 @@ const { registerLoginMethod, registerSecondFactor } = const { loginRateLimiter } = await import("../../utils/login-rate-limiter.js"); const PASSWORD = "correct-horse"; -const totpSecret = speakeasy.generateSecret({ name: "test" }).base32; +// Stands in for the totp plugin's factor: core only sees the registration. +const TOTP_CODE = "123456"; +let disposeTotp: (() => void) | null = null; +function registerTotpFixture() { + disposeTotp = registerSecondFactor({ + id: "totp", + pluginId: "totp", + labelKey: "totp:factor", + isEnrolled: async () => false, + verify: async (_userId, body) => body.totp_code === TOTP_CODE, + reset: async () => {}, + }); +} function addUser( overrides: Partial = {}, @@ -109,7 +120,6 @@ function addUser( passwordHash: bcrypt.hashSync(PASSWORD, 4), isAdmin: false, isOidc: false, - totpEnabled: false, ...overrides, }; h.state.users.set(user.id, user); @@ -124,6 +134,8 @@ beforeEach(() => { loginRateLimiter.resetAttempts("10.0.0.1", "alice"); loginRateLimiter.resetTOTPAttempts("u1"); delete process.env.ALLOW_PASSWORD_LOGIN; + disposeTotp?.(); + registerTotpFixture(); }); async function passwordLogin(body: Record, headers = {}) { @@ -241,12 +253,8 @@ describe("password login through the pipeline", () => { describe("second factors", () => { function enrolTotp() { - addUser({ - totpEnabled: true, - totpSecret, - totpBackupCodes: JSON.stringify(["BACKUP01"]), - }); - h.state.factors.push({ userId: "u1", pluginId: "core", factorId: "totp" }); + addUser(); + h.state.factors.push({ userId: "u1", pluginId: "totp", factorId: "totp" }); } it("stops for TOTP with the pending token the old route returned", async () => { @@ -259,7 +267,7 @@ describe("second factors", () => { temp_token: "pending-1", rememberMe: false, second_factors: [ - expect.objectContaining({ id: "totp", pluginId: "core" }), + expect.objectContaining({ id: "totp", pluginId: "totp" }), ], }); expect(h.manager.generateJWTToken).toHaveBeenCalledWith("u1", { @@ -280,7 +288,7 @@ describe("second factors", () => { const req = fakeRequest({ body: { temp_token: tempToken, - totp_code: speakeasy.totp({ secret: totpSecret, encoding: "base32" }), + totp_code: TOTP_CODE, rememberMe: true, }, }); @@ -298,24 +306,40 @@ describe("second factors", () => { expect(h.state.audits.at(-1)).toMatchObject({ action: "login" }); }); - it("accepts a backup code once, stored encrypted", async () => { + it("answers the 2.8 route with the user's first factor when none is named", async () => { enrolTotp(); const first = await passwordLogin({ username: "alice", password: PASSWORD, }); - const tempToken = (first.body as { temp_token: string }).temp_token; const res = fakeResponse(); await verifySecondFactorAndRespond( fakeRequest({ - body: { temp_token: tempToken, totp_code: "BACKUP01" }, + body: { + temp_token: (first.body as { temp_token: string }).temp_token, + totp_code: TOTP_CODE, + }, }) as never, res as never, - "totp", ); expect(res.statusCode).toBe(200); - // Re-encrypted after use, and the code is gone. - expect(h.state.users.get("u1")!.totpBackupCodes).not.toContain("BACKUP01"); + }); + + it("fails closed for a 2.8 TOTP user while the totp plugin is disabled", async () => { + enrolTotp(); + disposeTotp?.(); + disposeTotp = null; + const req = fakeRequest({ + body: { username: "alice", password: PASSWORD }, + }); + const identity = await verifyPasswordLogin(req as never); + await expect( + runLogin(req as never, identity, { + methodId: "password", + rememberMe: false, + }), + ).rejects.toMatchObject({ status: 403, code: "second_factor_unavailable" }); + expect(h.manager.generateJWTToken).not.toHaveBeenCalled(); }); it("still stops password login for TOTP when the external-login setting is off", async () => { @@ -562,9 +586,8 @@ describe("external identities", () => { username: "Alice", isOidc: true, passwordHash: "", - totpEnabled: true, - totpSecret, }); + h.state.factors.push({ userId: "u9", pluginId: "totp", factorId: "totp" }); h.state.identities.push({ id: 1, userId: "u9", @@ -586,9 +609,8 @@ describe("external identities", () => { username: "Alice", isOidc: true, passwordHash: "", - totpEnabled: true, - totpSecret, }); + h.state.factors.push({ userId: "u9", pluginId: "totp", factorId: "totp" }); h.state.identities.push({ id: 1, userId: "u9", @@ -658,9 +680,8 @@ describe("redirect logins", () => { username: "Alice", isOidc: true, passwordHash: "", - totpEnabled: true, - totpSecret, }); + h.state.factors.push({ userId: "u9", pluginId: "totp", factorId: "totp" }); h.state.identities.push({ id: 1, userId: "u9", @@ -686,7 +707,7 @@ describe("redirect logins", () => { fakeRequest({ cookies: { termix_pending_login: res.cookies[0].value }, body: { - totp_code: speakeasy.totp({ secret: totpSecret, encoding: "base32" }), + totp_code: TOTP_CODE, }, }) as never, verify as never, diff --git a/src/backend/tests/database/repositories/user-session-repositories.test.ts b/src/backend/tests/database/repositories/user-session-repositories.test.ts index aee820a6b..f834c3884 100644 --- a/src/backend/tests/database/repositories/user-session-repositories.test.ts +++ b/src/backend/tests/database/repositories/user-session-repositories.test.ts @@ -54,7 +54,6 @@ describe("UserRepository and SessionRepository", () => { }); expect(await repo.users.countAdmins()).toBe(1); - expect(await repo.users.countTotpEnabled()).toBe(0); expect((await repo.users.listAll()).map((user) => user.id)).toEqual([ "user-1", ]); @@ -66,10 +65,8 @@ describe("UserRepository and SessionRepository", () => { const updated = await repo.users.update("user-1", { oidcIdentifier: "oidc:admin", - totpEnabled: true, }); expect(updated?.oidcIdentifier).toBe("oidc:admin"); - expect(await repo.users.countTotpEnabled()).toBe(1); expect((await repo.users.findByOidcIdentifier("oidc:admin"))?.id).toBe( "user-1", ); diff --git a/src/backend/tests/database/routes/desktop-auto-session.test.ts b/src/backend/tests/database/routes/desktop-auto-session.test.ts index af963e137..3f0685fea 100644 --- a/src/backend/tests/database/routes/desktop-auto-session.test.ts +++ b/src/backend/tests/database/routes/desktop-auto-session.test.ts @@ -14,7 +14,6 @@ function makeUser(overrides: Partial = {}): UserRecord { username: "local", passwordHash: "", isOidc: false, - totpEnabled: false, isAdmin: false, registeredAt: "2026-01-01T00:00:00.000Z", ...overrides, @@ -146,11 +145,6 @@ describe("resolveDesktopAutoSessionUser", () => { expect(resolveDesktopAutoSessionUser([user])).toBe(user); }); - it("returns the sole local user even when TOTP-enabled", () => { - const user = makeUser({ totpEnabled: true }); - expect(resolveDesktopAutoSessionUser([user])).toBe(user); - }); - it("returns the auto-provisioned passwordless placeholder", () => { const user = makeUser({ passwordHash: "" }); expect(resolveDesktopAutoSessionUser([user])).toBe(user); diff --git a/src/backend/tests/database/routes/totp-disable-route.test.ts b/src/backend/tests/database/routes/totp-disable-route.test.ts deleted file mode 100644 index a0a349364..000000000 --- a/src/backend/tests/database/routes/totp-disable-route.test.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import bcrypt from "bcryptjs"; -import speakeasy from "speakeasy"; - -const userUpdate = vi.fn().mockResolvedValue(null); -const findById = vi.fn(); - -vi.mock("../../../database/repositories/factory.js", () => ({ - createCurrentUserRepository: () => ({ findById, update: userUpdate }), - createCurrentTrustedDeviceRepository: () => ({}), - createCurrentUserSessionRepository: () => ({}), - createCurrentUserAuthRepository: () => ({ - removeSecondFactor: vi.fn().mockResolvedValue(true), - recordSecondFactor: vi.fn().mockResolvedValue(undefined), - }), -})); - -vi.mock("../../../utils/logger.js", () => ({ - authLogger: { - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - success: vi.fn(), - }, -})); - -vi.mock("../../../utils/database-save-trigger.js", () => ({ - DatabaseSaveTrigger: { forceSave: vi.fn().mockResolvedValue(undefined) }, -})); - -const { registerUserTotpRoutes } = - await import("../../../database/routes/user-totp-routes.js"); - -const secret = speakeasy.generateSecret({ name: "test" }).base32; -const PASSWORD = "correct-horse"; - -/** - * The disable dialog has one field, labelled "Enter TOTP code or password", - * and its caller passes that single value as `disableTOTP(input)` — which - * lands in the `password` argument, leaving `totp_code` undefined. - * - * 2.5.1 changed the route to require both, so from then on the first check - * rejected every attempt regardless of what was typed. Nobody could turn 2FA - * off, and the client reported it as the generic "Failed to disable 2FA". - */ -describe("POST /totp/disable", () => { - let handler: (req: unknown, res: unknown) => Promise; - let setupHandler: (req: unknown, res: unknown) => Promise; - - beforeEach(() => { - vi.clearAllMocks(); - - const routes = new Map unknown>(); - const router = { - post: (path: string, ...rest: unknown[]) => { - routes.set(path, rest[rest.length - 1] as never); - }, - get: () => {}, - put: () => {}, - delete: () => {}, - }; - - registerUserTotpRoutes( - router as never, - { - authenticateJWT: (() => {}) as never, - authManager: { getUserDataKey: () => null } as never, - isNativeAppRequest: () => false, - } as never, - ); - - handler = routes.get("/totp/disable") as never; - setupHandler = routes.get("/totp/setup") as never; - findById.mockResolvedValue({ - id: "user-1", - username: "test-user", - isOidc: false, - passwordHash: bcrypt.hashSync(PASSWORD, 4), - totpSecret: secret, - totpBackupCodes: JSON.stringify(["BACKUP01"]), - totpEnabled: true, - }); - }); - - function call(body: Record) { - const res = { - statusCode: 200, - body: undefined as unknown, - status(code: number) { - this.statusCode = code; - return this; - }, - json(payload: unknown) { - this.body = payload; - return this; - }, - }; - return handler({ userId: "user-1", body }, res).then(() => res); - } - - function callSetup(body: Record) { - const res = { - statusCode: 200, - body: undefined as unknown, - status(code: number) { - this.statusCode = code; - return this; - }, - json(payload: unknown) { - this.body = payload; - return this; - }, - }; - return setupHandler({ userId: "user-1", body }, res).then(() => res); - } - - it("reveals the existing enrollment only after re-authentication", async () => { - const denied = await callSetup({ credential: "wrong" }); - expect(denied.statusCode).toBe(401); - - const allowed = await callSetup({ credential: PASSWORD }); - expect(allowed.statusCode).toBe(200); - expect(allowed.body).toEqual( - expect.objectContaining({ secret, additional: true }), - ); - expect(userUpdate).not.toHaveBeenCalled(); - }); - - it("requires a credential before adding another authenticator", async () => { - const res = await callSetup({}); - expect(res.statusCode).toBe(400); - expect(userUpdate).not.toHaveBeenCalled(); - }); - - it("accepts the TOTP code on its own", async () => { - // What the dialog sends: one value, in whichever field the client used. - const res = await call({ - totp_code: speakeasy.totp({ secret, encoding: "base32" }), - }); - - expect(res.statusCode).toBe(200); - expect(userUpdate).toHaveBeenCalledWith( - "user-1", - expect.objectContaining({ totpEnabled: false, totpSecret: null }), - ); - }); - - it("accepts the account password on its own", async () => { - // The single field is labelled "TOTP code or password", and the client - // passes it as the password argument — this is the exact failing call. - const res = await call({ password: PASSWORD }); - - expect(res.statusCode).toBe(200); - expect(userUpdate).toHaveBeenCalled(); - }); - - it("accepts a backup code", async () => { - const res = await call({ totp_code: "BACKUP01" }); - - expect(res.statusCode).toBe(200); - }); - - it("still refuses a wrong value", async () => { - const res = await call({ password: "not-my-password" }); - - expect(res.statusCode).toBe(401); - expect(userUpdate).not.toHaveBeenCalled(); - }); - - it("refuses an empty request rather than disabling anything", async () => { - const res = await call({}); - - expect(res.statusCode).toBe(400); - expect(userUpdate).not.toHaveBeenCalled(); - }); - - it("does not let an OIDC user disable it with a password", async () => { - // No password to compare against; only a TOTP or backup code will do. - findById.mockResolvedValue({ - id: "user-1", - isOidc: true, - passwordHash: null, - totpSecret: secret, - totpBackupCodes: JSON.stringify([]), - totpEnabled: true, - }); - - const res = await call({ password: "anything" }); - - expect(res.statusCode).toBe(401); - expect(userUpdate).not.toHaveBeenCalled(); - }); -}); diff --git a/src/backend/tests/database/routes/user-admin-routes.test.ts b/src/backend/tests/database/routes/user-admin-routes.test.ts index c9b2a6954..5a943e769 100644 --- a/src/backend/tests/database/routes/user-admin-routes.test.ts +++ b/src/backend/tests/database/routes/user-admin-routes.test.ts @@ -11,7 +11,6 @@ const state = vi.hoisted(() => ({ isAdmin: boolean; isOidc: boolean; passwordHash: string | null; - totpEnabled: boolean; } >(), unlockedUsers: new Set(), @@ -94,6 +93,8 @@ vi.mock("../../../database/repositories/factory.js", () => ({ createCurrentUserAuthRepository: () => ({ listSecondFactors: async (userId: string) => state.secondFactors.filter((row) => row.userId === userId), + listUserIdsWithSecondFactors: async () => + new Set(state.secondFactors.map((row) => row.userId)), removeSecondFactor: async ( userId: string, pluginId: string, @@ -127,6 +128,7 @@ vi.mock("../../../database/repositories/factory.js", () => ({ const { registerUserAdminRoutes } = await import("../../../database/routes/user-admin-routes.js"); +const { registerSecondFactor } = await import("../../../auth/registry.js"); // Capture the handlers registered on the router so we can invoke them directly // without spinning up an HTTP server. @@ -225,7 +227,6 @@ beforeEach(() => { isAdmin: true, isOidc: false, passwordHash: "hash", - totpEnabled: false, }, ], [ @@ -236,7 +237,6 @@ beforeEach(() => { isAdmin: false, isOidc: false, passwordHash: "hash", - totpEnabled: true, }, ], [ @@ -247,7 +247,6 @@ beforeEach(() => { isAdmin: false, isOidc: false, passwordHash: "hash", - totpEnabled: false, }, ], ]); @@ -257,15 +256,19 @@ beforeEach(() => { }); describe("GET /list", () => { - it("includes data_unlocked and totp_enabled for admin callers", async () => { + it("includes data_unlocked and second_factor_enabled for admin callers", async () => { + state.secondFactors = [ + { userId: "target1", pluginId: "totp", factorId: "totp" }, + ]; const res = await invoke("get", "/list"); expect(res.statusCode).toBe(200); const users = (res.jsonBody as { users: Record[] }).users; const target = users.find((u) => u.userId === "target1")!; expect(target.data_unlocked).toBe(true); - expect(target.totp_enabled).toBe(true); + expect(target.second_factor_enabled).toBe(true); const locked = users.find((u) => u.userId === "locked1")!; expect(locked.data_unlocked).toBe(false); + expect(locked.second_factor_enabled).toBe(false); }); it("omits management fields for non-admin callers", async () => { @@ -273,7 +276,7 @@ describe("GET /list", () => { const res = await invoke("get", "/list"); const users = (res.jsonBody as { users: Record[] }).users; expect(users[0].data_unlocked).toBeUndefined(); - expect(users[0].totp_enabled).toBeUndefined(); + expect(users[0].second_factor_enabled).toBeUndefined(); }); it("returns every user when no limit is given", async () => { @@ -335,46 +338,6 @@ describe("GET /list", () => { }); }); -describe("POST /admin/totp/disable", () => { - it("clears TOTP fields for the target and audits", async () => { - const res = await invoke("post", "/admin/totp/disable", { - body: { userId: "target1" }, - }); - expect(res.statusCode).toBe(200); - const update = state.updates.find((u) => u.id === "target1"); - expect(update?.changes).toMatchObject({ - totpSecret: null, - totpEnabled: false, - totpBackupCodes: null, - }); - expect( - state.auditCalls.some((c) => c.action === "admin_disable_totp"), - ).toBe(true); - }); - - it("403s when the caller is not an admin", async () => { - state.currentUserId = "target1"; - const res = await invoke("post", "/admin/totp/disable", { - body: { userId: "locked1" }, - }); - expect(res.statusCode).toBe(403); - }); - - it("400s when TOTP is not enabled for the target", async () => { - const res = await invoke("post", "/admin/totp/disable", { - body: { userId: "locked1" }, - }); - expect(res.statusCode).toBe(400); - }); - - it("404s for an unknown target", async () => { - const res = await invoke("post", "/admin/totp/disable", { - body: { userId: "ghost" }, - }); - expect(res.statusCode).toBe(404); - }); -}); - describe("GET /admin/export/:userId", () => { it("423s when the target's data is locked", async () => { const res = await invoke("get", "/admin/export/:userId", { @@ -395,23 +358,31 @@ describe("GET /admin/export/:userId", () => { describe("DELETE /admin/:userId/second-factors", () => { it("clears every factor, including one whose plugin is gone, and audits", async () => { + const reset = vi.fn(async () => {}); + const dispose = registerSecondFactor({ + id: "totp", + pluginId: "totp", + labelKey: "factor", + isEnrolled: async () => false, + verify: async () => false, + reset, + }); state.secondFactors = [ - { userId: "target1", pluginId: "core", factorId: "totp" }, + { userId: "target1", pluginId: "totp", factorId: "totp" }, { userId: "target1", pluginId: "gone-plugin", factorId: "yubikey" }, - { userId: "locked1", pluginId: "core", factorId: "totp" }, + { userId: "locked1", pluginId: "totp", factorId: "totp" }, ]; const res = await invoke("delete", "/admin/:userId/second-factors", { params: { userId: "target1" }, }); + dispose(); expect(res.statusCode).toBe(200); expect(state.secondFactors).toEqual([ - { userId: "locked1", pluginId: "core", factorId: "totp" }, + { userId: "locked1", pluginId: "totp", factorId: "totp" }, ]); expect(state.trustedDevicesCleared).toContain("target1"); - // TOTP's own reset cleared its secret. - expect( - state.updates.find((u) => u.id === "target1")?.changes, - ).toMatchObject({ totpEnabled: false, totpSecret: null }); + // The running plugin's own reset dropped its secret. + expect(reset).toHaveBeenCalledWith("target1"); const audit = state.auditCalls.find( (c) => c.action === "admin_reset_second_factors", ); diff --git a/src/backend/tests/database/routes/user-password-reset-routes.test.ts b/src/backend/tests/database/routes/user-password-reset-routes.test.ts index 02262c544..546f50446 100644 --- a/src/backend/tests/database/routes/user-password-reset-routes.test.ts +++ b/src/backend/tests/database/routes/user-password-reset-routes.test.ts @@ -7,6 +7,7 @@ const calls = vi.hoisted(() => ({ rotatedFor: [] as string[], legacyWrapsDeletedFor: [] as string[], dataWipedEvents: [] as Array<{ userId: string }>, + secondFactorsReset: [] as string[], })); function deletingRepo(label: string) { @@ -59,6 +60,13 @@ vi.mock("../../../utils/crypto-migration/dek-migration.js", () => ({ }, })); +vi.mock("../../../auth/second-factor-admin.js", () => ({ + resetUserSecondFactors: async (userId: string) => { + calls.secondFactorsReset.push(userId); + return []; + }, +})); + import { resetUserPassword } from "../../../database/routes/user-password-reset-routes.js"; function fakeAuthManager(unlocked: boolean): AuthManager { @@ -70,6 +78,7 @@ function fakeAuthManager(unlocked: boolean): AuthManager { beforeEach(() => { calls.userUpdates = []; + calls.secondFactorsReset = []; calls.deletedFor = []; calls.rotatedFor = []; calls.legacyWrapsDeletedFor = []; @@ -124,8 +133,6 @@ describe("resetUserPassword", () => { expect(calls.dataWipedEvents).toEqual([{ userId: "user-1" }]); expect(calls.rotatedFor).toEqual(["user-1"]); expect(calls.legacyWrapsDeletedFor).toEqual(["user-1"]); - expect( - calls.userUpdates.some(([, update]) => update.totpEnabled === false), - ).toBe(true); + expect(calls.secondFactorsReset).toEqual(["user-1"]); }); }); diff --git a/src/backend/tests/database/routes/user-totp-routes.test.ts b/src/backend/tests/database/routes/user-totp-routes.test.ts deleted file mode 100644 index 58f535162..000000000 --- a/src/backend/tests/database/routes/user-totp-routes.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import bcrypt from "bcryptjs"; -import speakeasy from "speakeasy"; - -// The route module imports repository factories and the logger; stub both so -// importing stays inert. -const userRepositoryUpdate = vi.fn().mockResolvedValue(null); - -vi.mock("../../../database/repositories/factory.js", () => ({ - createCurrentUserRepository: () => ({ - update: userRepositoryUpdate, - }), -})); - -vi.mock("../../../utils/logger.js", () => ({ - authLogger: { - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - success: vi.fn(), - }, -})); - -const { verifyTotpReauth } = - await import("../../../database/routes/user-totp-routes.js"); - -type AnyUser = Parameters[0]; - -const secret = speakeasy.generateSecret({ name: "test" }).base32; - -function makeUser(overrides: Partial = {}): AnyUser { - return { - id: "user-1", - isOidc: false, - passwordHash: bcrypt.hashSync("correct-horse", 4), - totpSecret: secret, - totpBackupCodes: JSON.stringify(["BACKUP01", "BACKUP02"]), - totpEnabled: true, - ...overrides, - } as AnyUser; -} - -describe("verifyTotpReauth", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("does not accept the account password as a second factor", async () => { - expect(await verifyTotpReauth(makeUser(), "correct-horse")).toBe(false); - }); - - it("accepts a valid TOTP code without a password", async () => { - const token = speakeasy.totp({ secret, encoding: "base32" }); - expect(await verifyTotpReauth(makeUser(), token)).toBe(true); - }); - - it("accepts a valid backup code and consumes it", async () => { - const result = await verifyTotpReauth(makeUser(), "BACKUP01"); - expect(result).toBe(true); - expect(userRepositoryUpdate).toHaveBeenCalledWith("user-1", { - totpBackupCodes: JSON.stringify(["BACKUP02"]), - }); - }); - - it("rejects a wrong password / invalid code", async () => { - expect(await verifyTotpReauth(makeUser(), "wrong")).toBe(false); - expect(userRepositoryUpdate).not.toHaveBeenCalled(); - }); - - it("ignores the password path for OIDC users but still accepts TOTP", async () => { - const token = speakeasy.totp({ secret, encoding: "base32" }); - const oidcUser = makeUser({ isOidc: true, passwordHash: null }); - expect(await verifyTotpReauth(oidcUser, token)).toBe(true); - expect(await verifyTotpReauth(oidcUser, "anything")).toBe(false); - }); - - it("handles malformed backup-code JSON without throwing", async () => { - const user = makeUser({ totpBackupCodes: "not json" }); - expect(await verifyTotpReauth(user, "BACKUP01")).toBe(false); - }); -}); diff --git a/src/backend/tests/database/routes/user-webauthn-routes.test.ts b/src/backend/tests/database/routes/user-webauthn-routes.test.ts deleted file mode 100644 index a28d6f1bd..000000000 --- a/src/backend/tests/database/routes/user-webauthn-routes.test.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { Request, Response, Router } from "express"; -import type { AuthManager } from "../../../utils/auth-manager.js"; - -const mocks = vi.hoisted(() => ({ - verify: vi.fn(), - user: vi.fn(), - update: vi.fn(), - manager: { - authenticateWebAuthnUser: vi.fn(), - authenticateOIDCUser: vi.fn(), - isTrustedDevice: vi.fn(), - generateJWTToken: vi.fn(), - getSecureCookieOptions: vi.fn(), - getClearCookieOptions: vi.fn(), - }, -})); -vi.mock("../../../utils/auth-manager.js", () => ({ - AuthManager: { getInstance: () => mocks.manager }, -})); -vi.mock("../../../utils/audit-logger.js", () => ({ - logAudit: vi.fn(), - getRequestMeta: () => ({ ipAddress: "", userAgent: "" }), -})); -vi.mock("../../../hosts/internal-events.js", () => ({ - emitInternalEvent: vi.fn(), -})); -vi.mock("@simplewebauthn/server", () => ({ - generateAuthenticationOptions: vi - .fn() - .mockResolvedValue({ challenge: "challenge" }), - verifyAuthenticationResponse: mocks.verify, - generateRegistrationOptions: vi.fn(), - verifyRegistrationResponse: vi.fn(), -})); -vi.mock("../../../database/repositories/factory.js", () => ({ - createCurrentUserRepository: () => ({ findById: mocks.user }), - createCurrentUserAuthRepository: () => ({ - listSecondFactors: async () => [], - }), - createCurrentSettingsRepository: () => ({ get: async () => null }), - createCurrentWebauthnCredentialRepository: () => ({ - findByCredentialId: vi.fn().mockResolvedValue({ - id: 1, - credentialId: "key", - userId: "user", - publicKey: "YQ", - counter: 0, - transports: "[]", - }), - updateAuthState: mocks.update, - }), - getCurrentSettingValue: vi.fn(), -})); -vi.mock("../../../utils/logger.js", () => { - const log = { - warn: vi.fn(), - info: vi.fn(), - error: vi.fn(), - success: vi.fn(), - debug: vi.fn(), - }; - return { authLogger: log, sshLogger: log, logger: log, databaseLogger: log }; -}); -vi.mock("../../../utils/user-agent-parser.js", () => ({ - parseUserAgent: () => ({ type: "browser", deviceInfo: "test" }), - generateDeviceFingerprint: () => "device", - getDeviceId: () => "id", -})); -const { registerUserWebAuthnRoutes } = - await import("../../../database/routes/user-webauthn-routes.js"); -type Handler = (req: Request, res: Response) => Promise; -const handlers = new Map(); -const manager = mocks.manager; -registerUserWebAuthnRoutes( - { - post: (path: string, ...callbacks: Handler[]) => - handlers.set(path, callbacks.at(-1)!), - get: vi.fn(), - delete: vi.fn(), - } as unknown as Router, - { - authenticateJWT: vi.fn(), - authManager: manager as unknown as AuthManager, - isNativeAppRequest: () => false, - }, -); -function response() { - const res = { json: vi.fn(), status: vi.fn(), cookie: vi.fn() }; - res.status.mockReturnValue(res); - res.cookie.mockReturnValue(res); - return res; -} -async function login(userVerification: string) { - const options = response(); - await handlers.get("/webauthn/authenticate/options")!( - { - body: { userVerification }, - get: () => "https://termix.test", - } as unknown as Request, - options as unknown as Response, - ); - const challengeId = options.json.mock.calls[0][0].challengeId; - const res = response(); - await handlers.get("/webauthn/authenticate/verify")!( - { - body: { challengeId, response: { id: "key" }, rememberMe: true }, - get: () => undefined, - headers: {}, - socket: {}, - } as unknown as Request, - res as unknown as Response, - ); - return res; -} -beforeEach(() => { - vi.clearAllMocks(); - manager.authenticateWebAuthnUser.mockResolvedValue(true); - manager.authenticateOIDCUser.mockResolvedValue(true); - manager.generateJWTToken.mockResolvedValue("token"); - manager.getSecureCookieOptions.mockReturnValue({ httpOnly: true }); - manager.getClearCookieOptions.mockReturnValue({}); - mocks.user.mockResolvedValue({ - id: "user", - username: "alice", - totpEnabled: true, - }); - manager.isTrustedDevice.mockResolvedValue(false); - mocks.verify.mockResolvedValue({ - verified: true, - authenticationInfo: { userVerified: false, newCounter: 1 }, - }); -}); -describe("WebAuthn second factor policy", () => { - it.each(["discouraged", "preferred", "required"])( - "does not let request preference %s bypass TOTP without verified UV", - async (preference) => { - const res = await login(preference); - expect(res.json).toHaveBeenCalledWith( - expect.objectContaining({ requires_totp: true, temp_token: "token" }), - ); - expect(manager.generateJWTToken).toHaveBeenCalledWith("user", { - pendingTOTP: true, - expiresIn: "10m", - }); - expect(res.cookie).not.toHaveBeenCalled(); - }, - ); - it("allows verified user verification to satisfy TOTP", async () => { - mocks.verify.mockResolvedValue({ - verified: true, - authenticationInfo: { userVerified: true, newCounter: 1 }, - }); - const res = await login("preferred"); - expect(res.cookie).toHaveBeenCalledWith("jwt", "token", { httpOnly: true }); - expect(manager.isTrustedDevice).not.toHaveBeenCalled(); - }); - it("preserves trusted-device exemption", async () => { - manager.isTrustedDevice.mockResolvedValue(true); - const res = await login("discouraged"); - expect(manager.isTrustedDevice).toHaveBeenCalledWith("user", "device"); - expect(res.cookie).toHaveBeenCalled(); - }); - it("never creates a session for an invalid assertion", async () => { - mocks.verify.mockResolvedValue({ verified: false }); - const res = await login("preferred"); - expect(res.status).toHaveBeenCalledWith(401); - expect(manager.generateJWTToken).not.toHaveBeenCalled(); - expect(res.cookie).not.toHaveBeenCalled(); - }); - it("allows users without TOTP to log in with a possession-only key", async () => { - mocks.user.mockResolvedValue({ - id: "user", - username: "alice", - totpEnabled: false, - }); - expect((await login("discouraged")).cookie).toHaveBeenCalled(); - }); -}); diff --git a/src/backend/tests/plugins/ctx-secrets.test.ts b/src/backend/tests/plugins/ctx-secrets.test.ts index e3cf12f32..3b6c655b9 100644 --- a/src/backend/tests/plugins/ctx-secrets.test.ts +++ b/src/backend/tests/plugins/ctx-secrets.test.ts @@ -63,6 +63,7 @@ vi.mock("../../database/repositories/factory.js", () => ({ vi.mock("../../utils/system-secret-crypto.js", () => ({ encryptSystemSecret: async (value: string) => `sysenc:${value}`, decryptSystemSecret: async (value: string) => value.replace(/^sysenc:/, ""), + isSystemEncrypted: (value: string) => value.startsWith("sysenc:"), })); vi.mock("../../utils/audit-logger.js", () => ({ @@ -156,4 +157,20 @@ describe("ctx.secrets store", () => { }); expect(state.rows.size).toBe(0); }); + + it("seals without an acting user and refuses without secrets:own", async () => { + const denied = contextFor([]); + await expect(denied.secrets.seal("v")).rejects.toThrow(/secrets:own/); + await expect(denied.secrets.unseal("sysenc:v")).rejects.toThrow( + /secrets:own/, + ); + + const ctx = contextFor(["secrets:own"]); + const sealed = await ctx.secrets.seal("totp-secret"); + expect(sealed).toBe("sysenc:totp-secret"); + expect(await ctx.secrets.unseal(sealed)).toBe("totp-secret"); + // A plain string was never sealed and must not read back as one. + expect(await ctx.secrets.unseal("totp-secret")).toBeNull(); + expect(JSON.stringify(auditEntries)).not.toContain("totp-secret"); + }); }); diff --git a/src/backend/tests/plugins/ctx-ssh-auth.test.ts b/src/backend/tests/plugins/ctx-ssh-auth.test.ts index 5fcaeae1f..30e3d013c 100644 --- a/src/backend/tests/plugins/ctx-ssh-auth.test.ts +++ b/src/backend/tests/plugins/ctx-ssh-auth.test.ts @@ -9,6 +9,10 @@ const h = vi.hoisted(() => ({ cleared: [] as string[], clearedAll: 0, pooled: [] as string[], + sessionId: undefined as string | undefined, + revoked: [] as Array<{ userId: string; except?: string }>, + trustedCleared: [] as string[], + policyError: null as Error | null, })); vi.mock("../../utils/logger.js", () => { @@ -29,7 +33,15 @@ vi.mock("../../plugins/permissions.js", async () => { }, }; }); -vi.mock("../../plugins/actor.js", () => ({ getActor: () => h.actor })); +vi.mock("../../plugins/actor.js", () => ({ + getActor: () => h.actor, + getActorSessionId: () => h.sessionId, +})); +vi.mock("../../auth/core-auth.js", () => ({ + assertSecondFactorEnrollmentAllowed: () => { + if (h.policyError) throw h.policyError; + }, +})); vi.mock("../../hosts/ssh-connection-pool.js", () => ({ withConnection: async ( key: string, @@ -74,6 +86,17 @@ vi.mock("../../database/repositories/factory.js", () => ({ }, removeSecondFactor: async () => true, }), + createCurrentSessionRepository: () => ({ + revokeAllForUser: async (userId: string, except?: string) => { + h.revoked.push({ userId, except }); + return 0; + }, + }), + createCurrentTrustedDeviceRepository: () => ({ + deleteByUserId: async (userId: string) => { + h.trustedCleared.push(userId); + }, + }), })); const { createPluginAuth, createPluginSsh } = @@ -359,4 +382,41 @@ describe("ctx.auth", () => { expect(getLoginMethod("corp-sso")).toBeUndefined(); expect(getSecondFactor("fixture", "pin")).toBeUndefined(); }); + + it("revokes other sessions and trusted devices on enrolment, keeping the caller's session", async () => { + h.granted = new Set(["auth:provide"]); + h.factors = []; + h.revoked = []; + h.trustedCleared = []; + h.policyError = null; + h.actor = "user-1"; + h.sessionId = "session-a"; + const auth = createPluginAuth({ + manifest: manifest(["auth:provide"], { secondFactors: ["pin"] }), + bag: new DisposableBag("fixture"), + audit: vi.fn(async () => {}), + }); + await auth.recordEnrollment("user-1", "pin"); + expect(h.revoked).toEqual([{ userId: "user-1", except: "session-a" }]); + expect(h.trustedCleared).toEqual(["user-1"]); + h.actor = undefined; + h.sessionId = undefined; + }); + + it("refuses enrolment when core policy forbids second factors", async () => { + h.granted = new Set(["auth:provide"]); + h.factors = []; + const { LoginMethodError } = await import("@termix/plugin-sdk/backend"); + h.policyError = new LoginMethodError("password login is off", 409); + const auth = createPluginAuth({ + manifest: manifest(["auth:provide"], { secondFactors: ["pin"] }), + bag: new DisposableBag("fixture"), + audit: vi.fn(async () => {}), + }); + await expect(auth.recordEnrollment("user-1", "pin")).rejects.toMatchObject({ + status: 409, + }); + expect(h.factors).toEqual([]); + h.policyError = null; + }); }); diff --git a/src/backend/tests/utils/crypto-migration/external-identity-migration.test.ts b/src/backend/tests/utils/crypto-migration/external-identity-migration.test.ts index 2936a0646..cf4307cd0 100644 --- a/src/backend/tests/utils/crypto-migration/external-identity-migration.test.ts +++ b/src/backend/tests/utils/crypto-migration/external-identity-migration.test.ts @@ -74,24 +74,19 @@ describe("parseLegacyIdentifier", () => { }); describe("runExternalIdentityMigration", () => { - it("moves every identifier and TOTP enrolment without touching the old data", async () => { + it("moves every identifier without touching the old data", async () => { user("ldap-user", { oidcIdentifier: "ldap:4:bob", ssoProviderId: 4 }); user("gh-user", { oidcIdentifier: "github:7:42", ssoProviderId: 7 }); user("oidc-user", { oidcIdentifier: "sub-123", ssoProviderId: 3, - totpEnabled: true, }); user("env-user", { oidcIdentifier: "someone@example.com" }); - user("local", { - isOidc: false, - passwordHash: "hash", - totpEnabled: true, - }); + user("local", { isOidc: false, passwordHash: "hash" }); const result = await runExternalIdentityMigration(); - expect(result).toEqual({ identities: 4, factors: 2, skipped: 0 }); + expect(result).toEqual({ identities: 4, skipped: 0 }); expect( h.state.identities.map(({ userId, providerId, subject }) => ({ userId, @@ -108,21 +103,15 @@ describe("runExternalIdentityMigration", () => { subject: "someone@example.com", }, ]); - expect(h.state.factors).toEqual([ - { userId: "oidc-user", pluginId: "core", factorId: "totp" }, - { userId: "local", pluginId: "core", factorId: "totp" }, - ]); - // Lossless: the old column and flag stay. + // Lossless: the old column stays. expect(h.state.users.get("ldap-user")!.oidcIdentifier).toBe("ldap:4:bob"); - expect(h.state.users.get("local")!.totpEnabled).toBe(true); }); it("is idempotent", async () => { - user("ldap-user", { oidcIdentifier: "ldap:4:bob", totpEnabled: true }); + user("ldap-user", { oidcIdentifier: "ldap:4:bob" }); await runExternalIdentityMigration(); const second = await runExternalIdentityMigration(); - expect(second).toEqual({ identities: 0, factors: 0, skipped: 2 }); + expect(second).toEqual({ identities: 0, skipped: 1 }); expect(h.state.identities).toHaveLength(1); - expect(h.state.factors).toHaveLength(1); }); }); diff --git a/src/backend/tests/utils/totp-migration.test.ts b/src/backend/tests/utils/totp-migration.test.ts new file mode 100644 index 000000000..3ecdb877a --- /dev/null +++ b/src/backend/tests/utils/totp-migration.test.ts @@ -0,0 +1,196 @@ +/** + * The 2.8 to 2.9 TOTP move. A user who had TOTP on must still be asked for it + * after upgrading: their enrolment row points at the totp plugin, and their + * secret and backup codes reach the plugin's table readable. Running it twice + * changes nothing, and without the plugin's table the enrolment still moves + * so the login fails closed. + */ + +import crypto from "node:crypto"; +import Database from "better-sqlite3"; +import { drizzle } from "drizzle-orm/better-sqlite3"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { FieldCrypto } from "../../utils/field-crypto.js"; + +const h = vi.hoisted(() => ({ + db: null as unknown, + deks: new Map(), +})); + +vi.mock("../../database/db/index.js", () => ({ getDb: () => h.db })); +vi.mock("../../utils/data-crypto.js", () => ({ + DataCrypto: { getUserDataKey: (userId: string) => h.deks.get(userId) }, +})); +vi.mock("../../utils/system-secret-crypto.js", () => ({ + encryptSystemSecret: async (value: string) => `sysenc:${value}`, +})); +vi.mock("../../utils/logger.js", () => { + const log = { info: vi.fn(), warn: vi.fn(), error: vi.fn() }; + return { databaseLogger: log }; +}); + +const { runTotpMigration } = + await import("../../utils/crypto-migration/totp-migration.js"); + +let sqlite: Database.Database; + +function setup({ pluginTable }: { pluginTable: boolean }) { + sqlite = new Database(":memory:"); + sqlite.exec(` + CREATE TABLE users ( + id TEXT PRIMARY KEY, + username TEXT NOT NULL, + totp_secret TEXT, + totp_enabled INTEGER NOT NULL DEFAULT 0, + totp_backup_codes TEXT + ); + CREATE TABLE user_second_factors ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + plugin_id TEXT NOT NULL, + factor_id TEXT NOT NULL, + enrolled_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE (user_id, plugin_id, factor_id) + ); + `); + if (pluginTable) { + sqlite.exec(` + CREATE TABLE p_totp_enrollments ( + user_id TEXT PRIMARY KEY NOT NULL, + secret TEXT, + pending_secret TEXT, + backup_codes TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + `); + } + h.db = drizzle(sqlite); +} + +function addTotpUser(id: string, secret: string, codes: string[]) { + const dek = crypto.randomBytes(32); + h.deks.set(id, dek); + sqlite + .prepare( + "INSERT INTO users (id, username, totp_secret, totp_enabled, totp_backup_codes) VALUES (?, ?, ?, 1, ?)", + ) + .run( + id, + id, + FieldCrypto.encryptField(secret, dek, id, "totpSecret"), + FieldCrypto.encryptField( + JSON.stringify(codes), + dek, + id, + "totpBackupCodes", + ), + ); +} + +const factors = () => + sqlite + .prepare( + "SELECT user_id, plugin_id, factor_id FROM user_second_factors ORDER BY user_id", + ) + .all(); + +beforeEach(() => { + h.deks.clear(); +}); + +describe("runTotpMigration", () => { + it("keeps a 2.8 TOTP user behind their factor, with a readable secret", async () => { + setup({ pluginTable: true }); + addTotpUser("alice", "JBSWY3DPEHPK3PXP", ["AAAA1111", "BBBB2222"]); + addTotpUser("bob", "KRSXG5CTMVRXEZLU", []); + sqlite.exec(`INSERT INTO users (id, username) VALUES ('carol', 'carol')`); + // A8 recorded alice under core; bob only has the flag. + sqlite.exec( + `INSERT INTO user_second_factors (user_id, plugin_id, factor_id) VALUES ('alice', 'core', 'totp')`, + ); + + expect(await runTotpMigration()).toEqual({ factors: 2, secrets: 2 }); + + expect(factors()).toEqual([ + { user_id: "alice", plugin_id: "totp", factor_id: "totp" }, + { user_id: "bob", plugin_id: "totp", factor_id: "totp" }, + ]); + const alice = sqlite + .prepare("SELECT * FROM p_totp_enrollments WHERE user_id = 'alice'") + .get() as Record; + expect(alice.secret).toBe("sysenc:JBSWY3DPEHPK3PXP"); + expect(alice.backup_codes).toBe( + `sysenc:${JSON.stringify(["AAAA1111", "BBBB2222"])}`, + ); + expect(alice.pending_secret).toBeNull(); + expect( + sqlite + .prepare( + "SELECT totp_enabled, totp_secret, totp_backup_codes FROM users WHERE id = 'alice'", + ) + .get(), + ).toEqual({ totp_enabled: 0, totp_secret: null, totp_backup_codes: null }); + + // A second boot changes nothing. + expect(await runTotpMigration()).toEqual({ factors: 0, secrets: 0 }); + expect(factors()).toHaveLength(2); + expect( + sqlite.prepare("SELECT COUNT(*) AS n FROM p_totp_enrollments").get(), + ).toEqual({ n: 2 }); + }); + + it("moves the enrolment but keeps the columns while the plugin table is missing", async () => { + setup({ pluginTable: false }); + addTotpUser("alice", "JBSWY3DPEHPK3PXP", ["AAAA1111"]); + + expect(await runTotpMigration()).toEqual({ factors: 1, secrets: 0 }); + expect(factors()).toEqual([ + { user_id: "alice", plugin_id: "totp", factor_id: "totp" }, + ]); + const row = sqlite + .prepare("SELECT totp_enabled, totp_secret FROM users WHERE id = 'alice'") + .get() as { totp_enabled: number; totp_secret: string }; + expect(row.totp_enabled).toBe(1); + expect(row.totp_secret).toBeTruthy(); + expect(await runTotpMigration()).toEqual({ factors: 0, secrets: 0 }); + }); + + it("skips a user whose data key is unavailable and retries later", async () => { + setup({ pluginTable: true }); + addTotpUser("alice", "JBSWY3DPEHPK3PXP", []); + const dek = h.deks.get("alice")!; + h.deks.delete("alice"); + + expect(await runTotpMigration()).toEqual({ factors: 1, secrets: 0 }); + h.deks.set("alice", dek); + expect(await runTotpMigration()).toEqual({ factors: 0, secrets: 1 }); + }); + + it("drops a duplicate core row when the plugin row already exists", async () => { + setup({ pluginTable: true }); + sqlite.exec(` + INSERT INTO users (id, username) VALUES ('dave', 'dave'); + INSERT INTO user_second_factors (user_id, plugin_id, factor_id) VALUES ('dave', 'core', 'totp'); + INSERT INTO user_second_factors (user_id, plugin_id, factor_id) VALUES ('dave', 'totp', 'totp'); + `); + await runTotpMigration(); + expect(factors()).toEqual([ + { user_id: "dave", plugin_id: "totp", factor_id: "totp" }, + ]); + }); + + it("does nothing on a fresh install without the legacy columns", async () => { + sqlite = new Database(":memory:"); + sqlite.exec(` + CREATE TABLE users (id TEXT PRIMARY KEY, username TEXT NOT NULL); + CREATE TABLE user_second_factors ( + id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, + plugin_id TEXT NOT NULL, factor_id TEXT NOT NULL, + enrolled_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + `); + h.db = drizzle(sqlite); + expect(await runTotpMigration()).toEqual({ factors: 0, secrets: 0 }); + }); +}); diff --git a/src/backend/tests/utils/user-encryption-migration-store.test.ts b/src/backend/tests/utils/user-encryption-migration-store.test.ts index 0f37c5210..20308e1d1 100644 --- a/src/backend/tests/utils/user-encryption-migration-store.test.ts +++ b/src/backend/tests/utils/user-encryption-migration-store.test.ts @@ -12,12 +12,11 @@ describe("RawSqliteUserEncryptionMigrationStore", () => { } it("owns legacy user encryption migration reads", () => { - const { db, all, get } = createDb(); + const { db, all } = createDb(); const store = new RawSqliteUserEncryptionMigrationStore(db); expect(store.listHostRecords("user-1")).toEqual([{ id: 1 }]); expect(store.listCredentialRecords("user-1")).toEqual([{ id: 1 }]); - expect(store.getUserRecord("user-1")).toEqual({ id: "user-1" }); expect(db.prepare).toHaveBeenCalledWith( "SELECT * FROM ssh_data WHERE user_id = ?", @@ -25,9 +24,7 @@ describe("RawSqliteUserEncryptionMigrationStore", () => { expect(db.prepare).toHaveBeenCalledWith( "SELECT * FROM ssh_credentials WHERE user_id = ?", ); - expect(db.prepare).toHaveBeenCalledWith("SELECT * FROM users WHERE id = ?"); expect(all).toHaveBeenCalledWith("user-1"); - expect(get).toHaveBeenCalledWith("user-1"); }); it("owns legacy sensitive field update statements", () => { @@ -52,12 +49,6 @@ describe("RawSqliteUserEncryptionMigrationStore", () => { public_key: "pub", key_type: "rsa", }); - store.updateUserSensitiveFields("user-1", { - totp_secret: "totp", - totp_backup_codes: "codes", - client_secret: "client", - oidc_identifier: "oidc", - }); expect(db.prepare).toHaveBeenCalledWith( expect.stringContaining("UPDATE ssh_data"), @@ -65,9 +56,6 @@ describe("RawSqliteUserEncryptionMigrationStore", () => { expect(db.prepare).toHaveBeenCalledWith( expect.stringContaining("UPDATE ssh_credentials"), ); - expect(db.prepare).toHaveBeenCalledWith( - expect.stringContaining("UPDATE users"), - ); expect(run).toHaveBeenCalledWith( "p", "k", @@ -80,13 +68,6 @@ describe("RawSqliteUserEncryptionMigrationStore", () => { 12, ); expect(run).toHaveBeenCalledWith("p", "k", "kp", "priv", "pub", "rsa", 13); - expect(run).toHaveBeenCalledWith( - "totp", - "codes", - "client", - "oidc", - "user-1", - ); }); it("owns password-reset dynamic field updates", () => { diff --git a/src/backend/utils/auth-manager.ts b/src/backend/utils/auth-manager.ts index 33c6a443c..147aaae97 100644 --- a/src/backend/utils/auth-manager.ts +++ b/src/backend/utils/auth-manager.ts @@ -168,7 +168,11 @@ class AuthManager { return this.ensureUserDEK(userId); } - async authenticateWebAuthnUser( + /** + * Opens the user's data key with the server-held wrapping, for a sign-in + * that carries no password (a passkey, trusted proxy login). + */ + async unlockWithSystemKey( userId: string, _deviceType?: DeviceType, ): Promise { diff --git a/src/backend/utils/crypto-migration/external-identity-migration.ts b/src/backend/utils/crypto-migration/external-identity-migration.ts index 67946caeb..a238e1a10 100644 --- a/src/backend/utils/crypto-migration/external-identity-migration.ts +++ b/src/backend/utils/crypto-migration/external-identity-migration.ts @@ -1,15 +1,13 @@ /** - * Moves sign-in identities and 2FA enrolment into their own tables. + * Moves sign-in identities into their own table. * * users.oidc_identifier packed provider and subject into one string: * "ldap::", "github::", or a bare OIDC subject * whose provider was users.sso_provider_id. Each becomes a - * user_external_identities row. users.totp_enabled becomes a - * user_second_factors row for core's TOTP factor, so a login can tell a user - * has a factor even after the plugin that runs it is gone. + * user_external_identities row. TOTP enrolment moves in totp-migration.ts. * - * Lossless: the old column and flag are left as they are. Idempotent: rows - * that already exist are skipped, so it runs on every boot. + * Lossless: the old column is left as it is. Idempotent: rows that already + * exist are skipped, so it runs on every boot. */ import { databaseLogger } from "../logger.js"; @@ -23,7 +21,6 @@ const LEGACY_OIDC_PROVIDER_ID = "legacy-oidc"; export interface ExternalIdentityMigrationResult { identities: number; - factors: number; skipped: number; } @@ -58,7 +55,6 @@ export function parseLegacyIdentifier( export async function runExternalIdentityMigration(): Promise { const result: ExternalIdentityMigrationResult = { identities: 0, - factors: 0, skipped: 0, }; @@ -89,24 +85,10 @@ export async function runExternalIdentityMigration(): Promise row.pluginId === "core" && row.factorId === "totp", - ) - ) { - result.skipped++; - } else { - await auth.recordSecondFactor(user.id, "core", "totp"); - result.factors++; - } - } } - if (result.identities > 0 || result.factors > 0) { - databaseLogger.info("Moved sign-in identities and 2FA enrolment", { + if (result.identities > 0) { + databaseLogger.info("Moved sign-in identities", { operation: "external_identity_migration", ...result, }); diff --git a/src/backend/utils/crypto-migration/totp-migration.ts b/src/backend/utils/crypto-migration/totp-migration.ts new file mode 100644 index 000000000..3cd38b2ff --- /dev/null +++ b/src/backend/utils/crypto-migration/totp-migration.ts @@ -0,0 +1,168 @@ +/** + * Moves 2.8 TOTP enrolments into the totp plugin. + * + * 1. Every user_second_factors row A8 wrote for core's TOTP ("core"/"totp") + * becomes the plugin's ("totp"/"totp"), and every user with + * users.totp_enabled gets one. This runs whether or not the plugin is + * enabled, so those users stay behind a second factor: with the plugin off + * the login fails closed instead of skipping it. + * 2. Once the plugin's p_totp_enrollments table exists, each enabled user's + * secret and backup codes are decrypted with their data key and written + * there sealed with the installation key (what ctx.secrets.seal writes), + * then the users columns are cleared. + * + * The columns stay in the database until 3.0.0, since core's drizzle + * migrations run before this does. A user whose data key cannot be opened + * at boot is skipped and tried again on the next one. Idempotent. + */ + +import { sql } from "drizzle-orm"; +import { databaseLogger } from "../logger.js"; +import { DataCrypto } from "../data-crypto.js"; +import { LazyFieldEncryption } from "../lazy-field-encryption.js"; +import { encryptSystemSecret } from "../system-secret-crypto.js"; +import { runStatement, selectRows } from "./raw-rows.js"; + +const PLUGIN_ID = "totp"; +const FACTOR_ID = "totp"; +const ENROLLMENTS_TABLE = "p_totp_enrollments"; + +export interface TotpMigrationResult { + /** user_second_factors rows written or moved to the plugin. */ + factors: number; + /** Secrets moved into the plugin's table. */ + secrets: number; +} + +interface LegacyTotpUser { + id: string; + totp_enabled: unknown; + totp_secret: string | null; + totp_backup_codes: string | null; +} + +async function tryRows(query: ReturnType): Promise { + try { + return await selectRows(query); + } catch { + // A fresh install never had the column or the table. + return null; + } +} + +function isOn(value: unknown): boolean { + return value === true || value === 1 || value === "1" || value === "t"; +} + +function readBackupCodes( + stored: string | null, + dek: Buffer, + userId: string, +): string[] { + if (!stored) return []; + const raw = LazyFieldEncryption.safeGetFieldValue( + stored, + dek, + userId, + "totpBackupCodes", + ); + try { + const codes = raw ? JSON.parse(raw) : []; + return Array.isArray(codes) + ? codes.filter((code): code is string => typeof code === "string") + : []; + } catch { + return []; + } +} + +async function factorUsers(pluginId: string): Promise> { + const rows = await selectRows<{ user_id: string }>( + sql`SELECT user_id FROM user_second_factors WHERE plugin_id = ${pluginId} AND factor_id = ${FACTOR_ID}`, + ); + return new Set(rows.map((row) => row.user_id)); +} + +export async function runTotpMigration(): Promise { + const result: TotpMigrationResult = { factors: 0, secrets: 0 }; + + try { + const enrolled = await factorUsers(PLUGIN_ID); + + for (const userId of await factorUsers("core")) { + if (enrolled.has(userId)) { + await runStatement( + sql`DELETE FROM user_second_factors WHERE user_id = ${userId} AND plugin_id = 'core' AND factor_id = ${FACTOR_ID}`, + ); + } else { + await runStatement( + sql`UPDATE user_second_factors SET plugin_id = ${PLUGIN_ID} WHERE user_id = ${userId} AND plugin_id = 'core' AND factor_id = ${FACTOR_ID}`, + ); + enrolled.add(userId); + result.factors++; + } + } + + const users = ( + (await tryRows( + sql`SELECT id, totp_enabled, totp_secret, totp_backup_codes FROM users`, + )) ?? [] + ).filter((user) => isOn(user.totp_enabled)); + + for (const user of users) { + if (enrolled.has(user.id)) continue; + await runStatement( + sql`INSERT INTO user_second_factors (user_id, plugin_id, factor_id) VALUES (${user.id}, ${PLUGIN_ID}, ${FACTOR_ID})`, + ); + enrolled.add(user.id); + result.factors++; + } + + const moved = await tryRows<{ user_id: string }>( + sql`SELECT user_id FROM ${sql.identifier(ENROLLMENTS_TABLE)}`, + ); + if (moved) { + const done = new Set(moved.map((row) => row.user_id)); + for (const user of users) { + if (done.has(user.id) || !user.totp_secret) continue; + try { + const dek = DataCrypto.getUserDataKey(user.id); + if (!dek) continue; + const secret = LazyFieldEncryption.safeGetFieldValue( + user.totp_secret, + dek, + user.id, + "totpSecret", + ); + if (!secret) continue; + const codes = readBackupCodes(user.totp_backup_codes, dek, user.id); + await runStatement( + sql`INSERT INTO ${sql.identifier(ENROLLMENTS_TABLE)} (user_id, secret, backup_codes) VALUES (${user.id}, ${await encryptSystemSecret(secret)}, ${await encryptSystemSecret(JSON.stringify(codes))})`, + ); + await runStatement( + sql`UPDATE users SET totp_enabled = FALSE, totp_secret = NULL, totp_backup_codes = NULL WHERE id = ${user.id}`, + ); + result.secrets++; + } catch (error) { + databaseLogger.warn("TOTP migration failed for a user", { + operation: "totp_migration", + userId: user.id, + error: error instanceof Error ? error.message : String(error), + }); + } + } + } + } catch (error) { + databaseLogger.error("TOTP migration failed", error, { + operation: "totp_migration", + }); + } + + if (result.factors > 0 || result.secrets > 0) { + databaseLogger.info("Moved TOTP enrolment into the totp plugin", { + operation: "totp_migration", + ...result, + }); + } + return result; +} diff --git a/src/backend/utils/data-crypto.ts b/src/backend/utils/data-crypto.ts index 8aa485cd0..8115c50f4 100644 --- a/src/backend/utils/data-crypto.ts +++ b/src/backend/utils/data-crypto.ts @@ -180,29 +180,6 @@ class DataCrypto { } } - const userRecord = store.getUserRecord(userId); - if (userRecord) { - const sensitiveFields = - LazyFieldEncryption.getSensitiveFieldsForTable("users"); - const { updatedRecord, migratedFields, needsUpdate } = - LazyFieldEncryption.migrateRecordSensitiveFields( - userRecord, - sensitiveFields, - userDataKey, - userId, - ); - - if (needsUpdate) { - store.updateUserSensitiveFields(userId, updatedRecord); - - migratedFieldsCount += migratedFields.length; - if (!migratedTables.includes("users")) { - migratedTables.push("users"); - } - migrated = true; - } - } - return { migrated, migratedTables, migratedFieldsCount }; } catch (error) { databaseLogger.error("User sensitive fields migration failed", error, { diff --git a/src/backend/utils/field-crypto.ts b/src/backend/utils/field-crypto.ts index 6cab1fc36..9ff60f35c 100644 --- a/src/backend/utils/field-crypto.ts +++ b/src/backend/utils/field-crypto.ts @@ -15,13 +15,7 @@ class FieldCrypto { private static readonly SALT_LENGTH = 32; private static readonly ENCRYPTED_FIELDS = { - users: new Set([ - "passwordHash", - "clientSecret", - "totpSecret", - "totpBackupCodes", - "oidcIdentifier", - ]), + users: new Set(["passwordHash", "clientSecret", "oidcIdentifier"]), ssh_data: new Set([ "password", "key", diff --git a/src/backend/utils/lazy-field-encryption.ts b/src/backend/utils/lazy-field-encryption.ts index 19b2bec44..2aef744b3 100644 --- a/src/backend/utils/lazy-field-encryption.ts +++ b/src/backend/utils/lazy-field-encryption.ts @@ -286,8 +286,6 @@ export class LazyFieldEncryption { rdpPassword: "rdp_password", vncPassword: "vnc_password", telnetPassword: "telnet_password", - totpSecret: "totp_secret", - totpBackupCodes: "totp_backup_codes", clientSecret: "client_secret", oidcIdentifier: "oidc_identifier", }; @@ -314,7 +312,6 @@ export class LazyFieldEncryption { "privateKey", "publicKey", ], - users: ["totpSecret", "totpBackupCodes"], }; return sensitiveFieldsMap[tableName] || []; @@ -439,33 +436,6 @@ export class LazyFieldEncryption { } } - const user = store.getUserRecord(userId); - if (user) { - const sensitiveFields = this.getSensitiveFieldsForTable("users"); - const userPlaintextFields: string[] = []; - - for (const field of sensitiveFields) { - const column = this.propertyToColumn(field); - const value = user[column]; - if ( - typeof value === "string" && - value && - this.fieldNeedsMigration(value, userKEK, userId, field) - ) { - userPlaintextFields.push(field); - needsMigration = true; - } - } - - if (userPlaintextFields.length > 0) { - plaintextFields.push({ - table: "users", - recordId: userId, - fields: userPlaintextFields, - }); - } - } - return { needsMigration, plaintextFields }; } catch (error) { databaseLogger.error("Failed to check user migration needs", error, { diff --git a/src/backend/utils/swagger.ts b/src/backend/utils/swagger.ts index 541e182af..72b246637 100644 --- a/src/backend/utils/swagger.ts +++ b/src/backend/utils/swagger.ts @@ -167,10 +167,6 @@ const swaggerOptions: SwaggerJSDocOptions = { name: "SSO", description: "Single sign-on provider configuration", }, - { - name: "WebAuthn", - description: "Passkey registration and authentication", - }, { name: "Vault", description: "HashiCorp Vault SSH signing profiles", diff --git a/src/backend/utils/user-encryption-migration-store.ts b/src/backend/utils/user-encryption-migration-store.ts index 112eb326a..3232ecce4 100644 --- a/src/backend/utils/user-encryption-migration-store.ts +++ b/src/backend/utils/user-encryption-migration-store.ts @@ -8,7 +8,6 @@ export interface UserEncryptionMigrationRecord { export interface UserEncryptionMigrationStore { listHostRecords(userId: string): UserEncryptionMigrationRecord[]; listCredentialRecords(userId: string): UserEncryptionMigrationRecord[]; - getUserRecord(userId: string): UserEncryptionMigrationRecord | undefined; updateHostSensitiveFields( recordId: number | string, record: Record, @@ -17,10 +16,6 @@ export interface UserEncryptionMigrationStore { recordId: number | string, record: Record, ): void; - updateUserSensitiveFields( - userId: string, - record: Record, - ): void; updatePasswordResetFields( table: "ssh_data" | "ssh_credentials" | "users", recordId: number | string, @@ -52,10 +47,6 @@ export class RawSqliteUserEncryptionMigrationStore implements UserEncryptionMigr .all(userId); } - getUserRecord(userId: string): UserEncryptionMigrationRecord | undefined { - return this.db.prepare("SELECT * FROM users WHERE id = ?").get(userId); - } - updateHostSensitiveFields( recordId: number | string, record: Record, @@ -104,27 +95,6 @@ export class RawSqliteUserEncryptionMigrationStore implements UserEncryptionMigr ); } - updateUserSensitiveFields( - userId: string, - record: Record, - ): void { - this.db - .prepare( - ` - UPDATE users - SET totp_secret = ?, totp_backup_codes = ?, client_secret = ?, oidc_identifier = ? - WHERE id = ? - `, - ) - .run( - record.totp_secret || null, - record.totp_backup_codes || null, - record.client_secret || null, - record.oidc_identifier || null, - userId, - ); - } - updatePasswordResetFields( table: "ssh_data" | "ssh_credentials" | "users", recordId: number | string, diff --git a/src/ui/api/system-status-api.ts b/src/ui/api/system-status-api.ts index 895813955..84e87b543 100644 --- a/src/ui/api/system-status-api.ts +++ b/src/ui/api/system-status-api.ts @@ -1,96 +1,9 @@ -import { AxiosError } from "axios"; import type { TermixAlert } from "@/types"; -import { - authApi, - handleApiError, - markUserAuthenticated, - type AuthResponse, -} from "@/main-axios"; +import { authApi, handleApiError } from "@/main-axios"; // ALERTS // ============================================================================ -export async function setupTOTP(credential?: string): Promise<{ - secret: string; - qr_code: string; - additional?: boolean; -}> { - try { - const response = await authApi.post("/users/totp/setup", { credential }); - return response.data; - } catch (error) { - handleApiError(error as AxiosError, "setup TOTP"); - throw error; - } -} - -export async function enableTOTP( - totp_code: string, -): Promise<{ message: string; backup_codes: string[] }> { - try { - const response = await authApi.post("/users/totp/enable", { totp_code }); - return response.data; - } catch (error) { - handleApiError(error as AxiosError, "enable TOTP"); - throw error; - } -} - -export async function disableTOTP( - password?: string, - totp_code?: string, -): Promise<{ message: string }> { - try { - const response = await authApi.post("/users/totp/disable", { - password, - totp_code, - }); - return response.data; - } catch (error) { - handleApiError(error as AxiosError, "disable TOTP"); - throw error; - } -} - -export async function verifyTOTPLogin( - temp_token: string, - totp_code: string, - rememberMe: boolean = false, -): Promise { - try { - const response = await authApi.post("/users/totp/verify-login", { - temp_token, - totp_code, - rememberMe, - }); - - if (response.data.success) { - markUserAuthenticated(); - } - - return response.data; - } catch (error) { - handleApiError(error as AxiosError, "verify TOTP login"); - throw error; - } -} - -export async function generateBackupCodes( - password?: string, - totp_code?: string, -): Promise<{ backup_codes: string[] }> { - try { - const response = await authApi.post("/users/totp/backup-codes", { - password, - totp_code, - }); - return response.data; - } catch (error) { - handleApiError(error as AxiosError, "generate backup codes"); - throw error; - } -} - export async function getUserAlerts(): Promise<{ alerts: TermixAlert[]; }> { diff --git a/src/ui/api/user-management-api.ts b/src/ui/api/user-management-api.ts index c5dc51862..709ac4166 100644 --- a/src/ui/api/user-management-api.ts +++ b/src/ui/api/user-management-api.ts @@ -194,19 +194,6 @@ export async function adminResetUserPassword( return response.data; } -export async function adminDisableUserTotp( - userId: string, -): Promise<{ message: string }> { - try { - const response = await authApi.post("/users/admin/totp/disable", { - userId, - }); - return response.data; - } catch (error) { - handleApiError(error, "disable user TOTP"); - } -} - export async function adminExportUserData( userId: string, ): Promise> { diff --git a/src/ui/api/webauthn-api.ts b/src/ui/api/webauthn-api.ts deleted file mode 100644 index 19b63877e..000000000 --- a/src/ui/api/webauthn-api.ts +++ /dev/null @@ -1,127 +0,0 @@ -import type { - AuthenticationResponseJSON, - PublicKeyCredentialCreationOptionsJSON, - PublicKeyCredentialRequestOptionsJSON, - RegistrationResponseJSON, -} from "@simplewebauthn/browser"; -import { - browserSupportsWebAuthn, - startAuthentication, - startRegistration, -} from "@simplewebauthn/browser"; -import { authApi, handleApiError } from "@/main-axios"; - -export type WebAuthnUserVerification = "discouraged" | "preferred" | "required"; - -export type WebAuthnCredentialSummary = { - id: string; - name: string; - deviceType?: string | null; - backedUp: boolean; - transports: string[]; - userVerification: WebAuthnUserVerification; - createdAt: string; - lastUsedAt?: string | null; -}; - -type RegistrationOptionsResponse = { - options: PublicKeyCredentialCreationOptionsJSON; - challengeId: string; -}; - -type AuthenticationOptionsResponse = { - options: PublicKeyCredentialRequestOptionsJSON; - challengeId: string; -}; - -export type PasskeyLoginResult = { - success: boolean; - requires_totp?: boolean; - temp_token?: string; - is_admin?: boolean; - username?: string; - userId?: string; - is_oidc?: boolean; - totp_enabled?: boolean; - token?: string; -}; - -export function isPasskeySupported(): boolean { - return browserSupportsWebAuthn(); -} - -export async function loginWithPasskey( - username?: string, - rememberMe = false, -): Promise { - try { - const optionsResponse = await authApi.post( - "/users/webauthn/authenticate/options", - username ? { username } : {}, - ); - const credential = await startAuthentication({ - optionsJSON: optionsResponse.data.options, - }); - const verifyResponse = await authApi.post( - "/users/webauthn/authenticate/verify", - { - challengeId: optionsResponse.data.challengeId, - response: credential as AuthenticationResponseJSON, - rememberMe, - }, - ); - return verifyResponse.data; - } catch (error) { - throw handleApiError(error, "sign in with passkey"); - } -} - -export async function listWebAuthnCredentials(): Promise<{ - credentials: WebAuthnCredentialSummary[]; -}> { - try { - const response = await authApi.get("/users/webauthn/credentials"); - return response.data; - } catch (error) { - throw handleApiError(error, "list passkeys"); - } -} - -export async function registerWebAuthnCredential( - name: string, - userVerification: WebAuthnUserVerification, -): Promise<{ success: boolean }> { - try { - const optionsResponse = await authApi.post( - "/users/webauthn/register/options", - { userVerification }, - ); - const credential = await startRegistration({ - optionsJSON: optionsResponse.data.options, - }); - const verifyResponse = await authApi.post( - "/users/webauthn/register/verify", - { - challengeId: optionsResponse.data.challengeId, - name, - response: credential as RegistrationResponseJSON, - }, - ); - return verifyResponse.data; - } catch (error) { - throw handleApiError(error, "register passkey"); - } -} - -export async function deleteWebAuthnCredential( - credentialId: string, -): Promise<{ success: boolean }> { - try { - const response = await authApi.delete( - `/users/webauthn/credentials/${credentialId}`, - ); - return response.data; - } catch (error) { - throw handleApiError(error, "delete passkey"); - } -} diff --git a/src/ui/auth/Auth.tsx b/src/ui/auth/Auth.tsx index 0f380a8ee..1b1673804 100644 --- a/src/ui/auth/Auth.tsx +++ b/src/ui/auth/Auth.tsx @@ -103,16 +103,9 @@ ensureLegacyAuthUI(); const STORAGE_KEY = "termix_auth"; -/** Methods drawn by the login form itself rather than the external tab. */ -const INLINE_METHODS = new Set(["password", "passkey"]); - -const DEFAULT_SECOND_FACTORS: SecondFactorRef[] = [ - { id: "totp", pluginId: "core", labelKey: "auth.twoFactorAuth" }, -]; - +/** An empty list means "whichever factors this screen has UI for". */ function parseSecondFactorIds(value: string | null): SecondFactorRef[] { const ids = (value ?? "").split(",").filter(Boolean); - if (ids.length === 0) return DEFAULT_SECOND_FACTORS; return ids.map((id) => ({ id, pluginId: "", labelKey: "" })); } const DESKTOP_MANUAL_LOGOUT_KEY = "termix_desktop_manual_logout"; @@ -153,7 +146,7 @@ function storeAuth(username: string) { ); } -type AuthView = "login" | "register" | "reset" | "totp" | "external"; +type AuthView = "login" | "register" | "reset" | "second-factor" | "external"; type ResetStep = "email" | "code" | "newpass"; interface AuthProps { @@ -272,12 +265,20 @@ export function Auth({ onLogin }: AuthProps) { // An empty token means the server holds the pending login in a cookie, // which is how a redirect login hands over to the second factor step. const [pendingToken, setPendingToken] = useState(""); - const [secondFactors, setSecondFactors] = useState( - DEFAULT_SECOND_FACTORS, - ); - const [activeFactorId, setActiveFactorId] = useState("totp"); + const [secondFactors, setSecondFactors] = useState([]); + const [activeFactorId, setActiveFactorId] = useState(""); const loginMethodUIs = useLoginMethods(); const secondFactorUIs = useSecondFactors(); + // A server that did not list its factors gets every factor with a UI here. + const shownFactors: SecondFactorRef[] = + secondFactors.length > 0 + ? secondFactors + : secondFactorUIs.map((ui) => ({ + id: ui.id, + pluginId: ui.pluginId ?? "", + labelKey: ui.titleKey, + })); + const currentFactorId = activeFactorId || shownFactors[0]?.id || ""; const [resetStep, setResetStep] = useState("email"); const [resetCode, setResetCode] = useState(""); @@ -301,10 +302,15 @@ export function Auth({ onLogin }: AuthProps) { const [passwordResetAllowed, setPasswordResetAllowed] = useState(true); const [authMethods, setAuthMethods] = useState([]); const [authMethodsLoaded, setAuthMethodsLoaded] = useState(false); + const inlineMethodIds = new Set( + loginMethodUIs.filter((ui) => ui.placement === "inline").map((ui) => ui.id), + ); + const inlineMethods = authMethods.filter((method) => + inlineMethodIds.has(method.id), + ); const externalMethods = authMethods.filter( - (method) => !INLINE_METHODS.has(method.id), + (method) => method.id !== "password" && !inlineMethodIds.has(method.id), ); - const passkeyMethod = authMethods.find((method) => method.id === "passkey"); const silentSigninHandledRef = useRef(false); const proxySigninHandledRef = useRef(false); const [oidcSilentLoginDefault, setOidcSilentLoginDefault] = useState(false); @@ -678,14 +684,13 @@ export function Auth({ onLogin }: AuthProps) { function enterSecondFactorStep( tempToken: string, - factors: SecondFactorRef[] = DEFAULT_SECOND_FACTORS, + factors: SecondFactorRef[] = [], ) { - const list = factors.length > 0 ? factors : DEFAULT_SECOND_FACTORS; setPendingToken(tempToken); - setSecondFactors(list); - setActiveFactorId(list[0].id); + setSecondFactors(factors); + setActiveFactorId(factors[0]?.id ?? ""); setLoading(false); - setView("totp"); + setView("second-factor"); } function showLoginError(err: unknown, fallbackKey: string) { @@ -835,12 +840,12 @@ export function Auth({ onLogin }: AuthProps) { async function verifyActiveFactor(body: Record) { setLoading(true); try { - const res = await verifySecondFactor(activeFactorId, { + const res = await verifySecondFactor(currentFactorId, { ...body, rememberMe, ...(pendingToken ? { temp_token: pendingToken } : {}), }); - await finishLogin(res, "totp_auth_component", { + await finishLogin(res, "second_factor_auth_component", { fallbackUsername: username, }); } catch (err: unknown) { @@ -1032,12 +1037,9 @@ export function Auth({ onLogin }: AuthProps) { startRedirect: (instanceId?: string) => startRedirect(method.id, instanceId), complete: (response: Record) => - completeMethodResponse( - response, - method.id === "passkey" - ? "passkey_auth_component" - : "method_auth_component", - ).catch((err) => showLoginError(err, "errors.loginFailed")), + completeMethodResponse(response).catch((err) => + showLoginError(err, "errors.loginFailed"), + ), }; if (ui) { const Component = ui.component; @@ -1330,7 +1332,7 @@ export function Auth({ onLogin }: AuthProps) {
{/* TOTP view */} - {view === "totp" && ( + {view === "second-factor" && (

@@ -1340,9 +1342,9 @@ export function Auth({ onLogin }: AuthProps) { {t("auth.enterCode")}

- {secondFactors.length > 1 && ( + {shownFactors.length > 1 && (
- {secondFactors.map((factor) => { + {shownFactors.map((factor) => { const ui = secondFactorUIs.find( (candidate) => candidate.id === factor.id, ); @@ -1351,7 +1353,7 @@ export function Auth({ onLogin }: AuthProps) { key={factor.id} type="button" onClick={() => setActiveFactorId(factor.id)} - className={`flex-1 py-2 text-xs font-bold uppercase tracking-widest transition-colors ${activeFactorId === factor.id ? "bg-accent-brand text-background" : "text-muted-foreground hover:text-foreground hover:bg-muted"}`} + className={`flex-1 py-2 text-xs font-bold uppercase tracking-widest transition-colors ${currentFactorId === factor.id ? "bg-accent-brand text-background" : "text-muted-foreground hover:text-foreground hover:bg-muted"}`} > {ui ? t(ui.titleKey) : factor.id} @@ -1361,7 +1363,7 @@ export function Auth({ onLogin }: AuthProps) { )} {(() => { const ui = secondFactorUIs.find( - (candidate) => candidate.id === activeFactorId, + (candidate) => candidate.id === currentFactorId, ); if (!ui) { return ( @@ -1373,14 +1375,14 @@ export function Auth({ onLogin }: AuthProps) { const Component = ui.component; return ( challengeSecondFactor( - activeFactorId, + currentFactorId, pendingToken || undefined, ) } @@ -1590,7 +1592,7 @@ export function Auth({ onLogin }: AuthProps) { {externalMethods.map((method) => renderLoginMethod(method), )} - {passkeyMethod && renderLoginMethod(passkeyMethod)} + {inlineMethods.map((method) => renderLoginMethod(method))}
)} @@ -1658,7 +1660,7 @@ export function Auth({ onLogin }: AuthProps) { )} - {passkeyMethod && renderLoginMethod(passkeyMethod)} + {inlineMethods.map((method) => renderLoginMethod(method))} )} diff --git a/src/ui/auth/ElectronLoginForm.tsx b/src/ui/auth/ElectronLoginForm.tsx index 3e107a74a..085cf890d 100644 --- a/src/ui/auth/ElectronLoginForm.tsx +++ b/src/ui/auth/ElectronLoginForm.tsx @@ -28,8 +28,7 @@ interface SaveRemoteSyncJwtResult { const AUTH_MESSAGE_SOURCES = new Set([ "auth_component", - "totp_auth_component", - "passkey_auth_component", + "second_factor_auth_component", "method_auth_component", "oidc_callback", ]); diff --git a/src/ui/auth/legacy-auth-ui.tsx b/src/ui/auth/legacy-auth-ui.tsx index 412203d22..6f6672c41 100644 --- a/src/ui/auth/legacy-auth-ui.tsx +++ b/src/ui/auth/legacy-auth-ui.tsx @@ -1,25 +1,20 @@ /** - * Login and second-factor UI that still lives in core for 2.9.0 (OIDC, LDAP, - * passkeys, TOTP), registered through the same registries a plugin uses. + * Login UI that still lives in core for 2.9.0 (OIDC, LDAP) and the + * certificate SSH auth editors, registered through the same registries a plugin uses. * Phase C moves each into its plugin by moving its block out of this file; * D1 deletes the file once it is empty. */ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; -import { ChevronDown, ChevronUp, Fingerprint } from "lucide-react"; -import type { - LoginMethodUIProps, - SecondFactorUIProps, -} from "@termix/plugin-sdk/frontend"; +import { ChevronDown, ChevronUp } from "lucide-react"; +import type { LoginMethodUIProps } from "@termix/plugin-sdk/frontend"; import { Button } from "@/components/button"; import { Input } from "@/components/input"; import { registerLoginMethod, - registerSecondFactor, registerSshAuthEditor, } from "@/plugin-host/auth-registry"; -import { isPasskeySupported, loginWithPasskey } from "@/api/webauthn-api"; const primaryButton = "w-full bg-accent-brand hover:bg-accent-brand/90 text-background font-bold"; @@ -147,142 +142,6 @@ function LdapLoginForms({ instances, disabled, submit }: LoginMethodUIProps) { ); } -function PasskeyLoginButton({ - rememberMe, - disabled, - complete, - username, -}: LoginMethodUIProps) { - const { t } = useTranslation(); - const [loading, setLoading] = useState(false); - const [supported] = useState(() => { - try { - return isPasskeySupported(); - } catch { - return false; - } - }); - if (!supported) return null; - return ( - - ); -} - -/** Six-digit codes, or a backup code of up to eight letters and digits. */ -export function normalizeTotpInput(value: string): string { - return value - .replace(/[^a-zA-Z0-9]/g, "") - .toUpperCase() - .slice(0, 8); -} - -export function isValidTotpInput(value: string): boolean { - return /^[A-Z0-9]{6,8}$/.test(value); -} - -function TotpChallenge({ disabled, verify, cancel }: SecondFactorUIProps) { - const { t } = useTranslation(); - const [code, setCode] = useState(""); - const [loading, setLoading] = useState(false); - const inputRef = useRef(null); - - useEffect(() => { - inputRef.current?.focus(); - }, []); - - return ( -
{ - event.preventDefault(); - if (!isValidTotpInput(code)) { - toast.error(t("auth.enterCode")); - return; - } - setLoading(true); - try { - await verify({ totp_code: code }); - } finally { - setLoading(false); - } - }} - className="flex flex-col gap-4" - > - -

- {t("auth.backupCodeHint")} -

- - -
- ); -} - function CertificateAuthInfo({ labelKey, descriptionKey, @@ -350,12 +209,6 @@ export function ensureLegacyAuthUI(): void { titleKey: "auth.loginWithLdap", component: LdapLoginForms, }); - registerLoginMethod({ - id: "passkey", - pluginId: "core", - titleKey: "auth.signInWithPasskey", - component: PasskeyLoginButton, - }); registerSshAuthEditor({ id: "opkssh", pluginId: "core", @@ -368,12 +221,6 @@ export function ensureLegacyAuthUI(): void { titleKey: "hosts.filterAuthStepca", component: StepCaAuthEditor, }); - registerSecondFactor({ - id: "totp", - pluginId: "core", - titleKey: "auth.twoFactorAuth", - component: TotpChallenge, - }); } /** Test helper. */ diff --git a/src/ui/locales/en.json b/src/ui/locales/en.json index 0c55be2f2..5786797f2 100644 --- a/src/ui/locales/en.json +++ b/src/ui/locales/en.json @@ -1467,8 +1467,6 @@ "hasAccount": "Already have an account?", "twoFactorAuth": "Two-Factor Authentication", "enterCode": "Enter verification code", - "backupCode": "Or use backup code", - "verifyCode": "Verify Code", "redirectingToApp": "Redirecting to app...", "sshAuthenticationRequired": "SSH Authentication Required", "sshNoKeyboardInteractive": "Keyboard-Interactive Authentication Unavailable", @@ -1512,15 +1510,11 @@ "authenticationDisabled": "Authentication Disabled", "authenticationDisabledDesc": "All authentication methods are currently disabled. Please contact your administrator.", "passwordLoginDisabledDesc": "Password login is disabled. Use a passkey or an external authentication provider.", - "signInWithPasskey": "Sign in with passkey", - "passkeyLoginFailed": "Passkey login failed", "attemptsRemaining": "{{count}} attempts remaining", "confirmResetDataWipe": "This account has not logged in since the encryption upgrade, so its stored data cannot be recovered without the old password. Resetting will permanently delete its hosts, credentials and snippets. Continue?", "loginWithSso": "Sign in with SSO", "loginWithLdap": "Sign in with LDAP", "password": "Password", - "totpFactor": "Authenticator code", - "backupCodeHint": "You can also enter one of your backup codes.", "secondFactorUnavailable": "Your account uses a second factor that is not available right now. Contact an admin to reset it.", "secondFactorNoUI": "This second factor cannot be shown on this screen. Contact an admin." }, @@ -1940,7 +1934,6 @@ "saving": "Saving...", "updateRegistrationFailed": "Failed to update registration setting", "updatePasswordLoginFailed": "Failed to update password login setting", - "cannotDisablePasswordLoginWithTotp": "Cannot disable password login while 2FA is enabled for one or more users. Disable 2FA first.", "updateOidcAutoProvisionFailed": "Failed to update OIDC auto-provision setting", "updateSecondFactorAfterExternalLoginFailed": "Failed to update second factor setting", "updateOidcSilentLoginDefaultFailed": "Failed to update silent OIDC login setting", @@ -1983,13 +1976,6 @@ "resetPasswordSuccessWiped": "Password reset. The user's encrypted data was wiped.", "resetPasswordFailed": "Failed to reset password", "resetPasswordConfirmWipe": "{{username}} has not logged in since the encryption upgrade, so their data cannot be recovered. Resetting now will delete their hosts, credentials and snippets. Continue?", - "totpSectionTitle": "Two-Factor Authentication", - "totpStatusEnabled": "TOTP is enabled for this user", - "totpStatusDisabled": "TOTP is not enabled for this user", - "disableTotp": "Disable", - "disableTotpConfirm": "Disable two-factor authentication for {{username}}? They will be able to log in with only their password.", - "totpDisabledSuccess": "TOTP disabled", - "totpDisableFailed": "Failed to disable TOTP", "manageApiKeys": "API Keys", "apiKeyNamePlaceholder": "Key name", "apiKeyCopyNotice": "Copy this key now, it won't be shown again.", @@ -2477,23 +2463,6 @@ "settingsUpdates": "Updates", "disableUpdateChecks": "Disable Update Checks", "disableUpdateChecksDesc": "Stop checking for updates", - "totpAuthenticator": "TOTP Authenticator", - "totpEnabled": "2FA is enabled", - "totpDisabled": "Add extra login security", - "totpAddAuthenticator": "Add device", - "totpAddTitle": "Add authenticator device", - "totpAddDescription": "Confirm your identity to enroll another authenticator with the same secure TOTP key.", - "totpAddInputRequired": "Enter your TOTP code or password", - "totpAddFailed": "Failed to add authenticator", - "totpAddScanInstructions": "Scan this QR code with the additional authenticator. Existing authenticators will continue to work.", - "totpAddSuccess": "Additional authenticator enrolled", - "disable": "Disable", - "enable": "Enable", - "setupTotp": "Setup TOTP", - "qrCode": "QR Code", - "totpInstructions": "Scan QR code or enter secret in your authenticator app, then enter the 6-digit code", - "totpCodePlaceholder": "000000", - "verify": "Verify", "changePassword": "Change Password", "currentPasswordLabel": "Current Password", "currentPasswordPlaceholder": "Current password", @@ -2539,34 +2508,7 @@ "roleUser": "User", "authMethodDual": "Dual Auth", "authMethodOidc": "OIDC", - "totpSetupFailed": "Failed to start TOTP setup", - "totpEnter6Digits": "Enter a 6-digit code", - "totpEnabledSuccess": "Two-factor authentication enabled", - "totpInvalidCode": "Invalid code, please try again", - "totpDisableInputRequired": "Enter your TOTP code or password", - "totpDisabledSuccess": "Two-factor authentication disabled", - "totpDisableFailed": "Failed to disable 2FA", - "totpDisableTitle": "Disable 2FA", - "totpDisablePlaceholder": "Enter TOTP code or password", - "totpDisableConfirm": "Disable 2FA", - "totpContinueVerify": "Continue to Verify", - "totpVerifyTitle": "Verify Code", - "totpBackupTitle": "Backup Codes", - "totpDownloadBackup": "Download Backup Codes", - "passkeys": "Passkeys", - "passkeysDesc": "Use WebAuthn/FIDO2 credentials to sign in without a password", - "passkeyName": "Passkey name", - "passkeyUvPreferred": "Preferred", - "passkeyUvRequired": "Required", - "passkeyUvDiscouraged": "Discouraged", - "addPasskey": "Add Passkey", - "noPasskeys": "No passkeys registered.", - "passkeyAdded": "Passkey added", - "passkeyAddFailed": "Failed to add passkey", - "passkeyDeleted": "Passkey deleted", - "passkeyDeleteFailed": "Failed to delete passkey", "done": "Done", - "secretCopied": "Secret copied to clipboard", "apiKeyNameRequired": "Key name is required", "apiKeyCreated": "API key \"{{name}}\" created", "apiKeyCreateFailed": "Failed to create API key", diff --git a/src/ui/locales/translated/af_ZA.json b/src/ui/locales/translated/af_ZA.json index 31a0d0277..1af0d3ce5 100644 --- a/src/ui/locales/translated/af_ZA.json +++ b/src/ui/locales/translated/af_ZA.json @@ -1592,8 +1592,6 @@ "hasAccount": "Het jy reeds 'n rekening?", "twoFactorAuth": "Twee-faktor-verifikasie", "enterCode": "Voer verifikasiekode in", - "backupCode": "Of gebruik rugsteunkode", - "verifyCode": "Verifieer Kode", "redirectingToApp": "Herlei na toepassing...", "sshAuthenticationRequired": "SSH-verifikasie vereis", "sshNoKeyboardInteractive": "Sleutelbord-interaktiewe verifikasie nie beskikbaar nie", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "Verifikasie gedeaktiveer", "authenticationDisabledDesc": "Alle verifikasiemetodes is tans gedeaktiveer. Kontak asseblief u administrateur.", "passwordLoginDisabledDesc": "Wagwoordaanmelding is gedeaktiveer. Gebruik 'n wagwoordsleutel of 'n eksterne verifikasieverskaffer.", - "signInWithPasskey": "Teken aan met wagwoord", - "passkeyLoginFailed": "Wagwoord-aanmelding het misluk", "attemptsRemaining": "{{count}} pogings oor", "confirmResetDataWipe": "Hierdie rekening het nie aangemeld sedert die enkripsie-opgradering nie, dus kan die gestoorde data nie sonder die ou wagwoord herstel word nie. Terugstelling sal die gashere, geloofsbriewe en brokkies permanent verwyder. Gaan voort?" }, @@ -2065,7 +2061,6 @@ "saving": "Stoor...", "updateRegistrationFailed": "Kon nie registrasie-instellings opdateer nie", "updatePasswordLoginFailed": "Kon nie wagwoord-aanmeldinstelling opdateer nie", - "cannotDisablePasswordLoginWithTotp": "Kan nie wagwoordaanmelding deaktiveer terwyl 2FA vir een of meer gebruikers geaktiveer is nie. Deaktiveer eers 2FA.", "updateOidcAutoProvisionFailed": "Kon nie OIDC-outovoorsieningsinstelling opdateer nie", "updateOidcSilentLoginDefaultFailed": "Kon nie stil OIDC-aanmeldinstelling opdateer nie", "updatePasswordResetFailed": "Kon nie wagwoordterugstellinginstelling opdateer nie", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "Wagwoord herstel. Die gebruiker se geïnkripteerde data is uitgevee.", "resetPasswordFailed": "Kon nie wagwoord terugstel nie", "resetPasswordConfirmWipe": "{{username}} het nie aangemeld sedert die enkripsie-opgradering nie, dus kan hul data nie herwin word nie. As u nou terugstel, sal hul gashere, geloofsbriewe en brokkies uitgevee word. Gaan voort?", - "totpSectionTitle": "Twee-faktor-verifikasie", - "totpStatusEnabled": "TOTP is vir hierdie gebruiker geaktiveer", - "totpStatusDisabled": "TOTP is nie vir hierdie gebruiker geaktiveer nie", - "disableTotp": "Deaktiveer", - "disableTotpConfirm": "Deaktiveer tweefaktor-verifikasie vir {{username}}? Hulle sal slegs met hul wagwoord kan aanmeld.", - "totpDisabledSuccess": "TOTP gedeaktiveer", - "totpDisableFailed": "Kon nie TOTP deaktiveer nie", "manageApiKeys": "API-sleutels", "apiKeyNamePlaceholder": "Sleutelnaam", "apiKeyCopyNotice": "Kopieer hierdie sleutel nou, dit sal nie weer gewys word nie.", @@ -2576,23 +2564,6 @@ "settingsUpdates": "Opdaterings", "disableUpdateChecks": "Deaktiveer opdateringskontroles", "disableUpdateChecksDesc": "Hou op om vir opdaterings te kyk", - "totpAuthenticator": "TOTP-verifikasie", - "totpEnabled": "2FA is geaktiveer", - "totpDisabled": "Voeg ekstra aanmeldsekuriteit by", - "totpAddAuthenticator": "Voeg toestel by", - "totpAddTitle": "Voeg verifikasietoestel by", - "totpAddDescription": "Bevestig jou identiteit om 'n ander outentiseerder met dieselfde veilige TOTP-sleutel in te skryf.", - "totpAddInputRequired": "Voer jou TOTP-kode of wagwoord in", - "totpAddFailed": "Kon nie verifieerder byvoeg nie", - "totpAddScanInstructions": "Skandeer hierdie QR-kode met die bykomende verifikator. Bestaande verifikators sal aanhou werk.", - "totpAddSuccess": "Bykomende outentiseerder ingeskryf", - "disable": "Deaktiveer", - "enable": "Aktiveer", - "setupTotp": "Stel TOTP op", - "qrCode": "QR-kode", - "totpInstructions": "Skandeer QR-kode of voer geheim in jou verifikasie-app in, en voer dan die 6-syferkode in", - "totpCodePlaceholder": "000000", - "verify": "Verifieer", "changePassword": "Verander wagwoord", "currentPasswordLabel": "Huidige Wagwoord", "currentPasswordPlaceholder": "Huidige wagwoord", @@ -2638,34 +2609,7 @@ "roleUser": "Gebruiker", "authMethodDual": "Dubbele Magtiging", "authMethodOidc": "OIDC", - "totpSetupFailed": "Kon nie TOTP-opstelling begin nie", - "totpEnter6Digits": "Voer 'n 6-syferkode in", - "totpEnabledSuccess": "Tweefaktor-verifikasie geaktiveer", - "totpInvalidCode": "Ongeldige kode, probeer asseblief weer", - "totpDisableInputRequired": "Voer jou TOTP-kode of wagwoord in", - "totpDisabledSuccess": "Tweefaktor-verifikasie gedeaktiveer", - "totpDisableFailed": "Kon nie 2FA deaktiveer nie", - "totpDisableTitle": "Deaktiveer 2FA", - "totpDisablePlaceholder": "Voer TOTP-kode of wagwoord in", - "totpDisableConfirm": "Deaktiveer 2FA", - "totpContinueVerify": "Gaan voort om te verifieer", - "totpVerifyTitle": "Verifieer Kode", - "totpBackupTitle": "Rugsteunkodes", - "totpDownloadBackup": "Laai rugsteunkodes af", - "passkeys": "Wagwoorde", - "passkeysDesc": "Gebruik WebAuthn/FIDO2-bewyse om aan te meld sonder 'n wagwoord", - "passkeyName": "Wagwoordnaam", - "passkeyUvPreferred": "Verkies", - "passkeyUvRequired": "Vereis", - "passkeyUvDiscouraged": "Ontmoedig", - "addPasskey": "Voeg wagwoord by", - "noPasskeys": "Geen wagwoorde geregistreer nie.", - "passkeyAdded": "Wagwoord bygevoeg", - "passkeyAddFailed": "Kon nie wagwoord byvoeg nie", - "passkeyDeleted": "Wagwoord verwyder", - "passkeyDeleteFailed": "Kon nie wagwoord verwyder nie", "done": "Klaar", - "secretCopied": "Geheim gekopieer na knipbord", "apiKeyNameRequired": "Sleutelnaam word vereis", "apiKeyCreated": "API-sleutel \"{{name}}\" geskep", "apiKeyCreateFailed": "Kon nie API-sleutel skep nie", diff --git a/src/ui/locales/translated/ar_SA.json b/src/ui/locales/translated/ar_SA.json index f87e79882..89aa64aea 100644 --- a/src/ui/locales/translated/ar_SA.json +++ b/src/ui/locales/translated/ar_SA.json @@ -1592,8 +1592,6 @@ "hasAccount": "هل لديك حساب بالفعل؟", "twoFactorAuth": "المصادقة الثنائية", "enterCode": "أدخل رمز التحقق", - "backupCode": "أو استخدم رمز النسخ الاحتياطي", - "verifyCode": "تحقق من الرمز", "redirectingToApp": "جارٍ إعادة التوجيه إلى التطبيق...", "sshAuthenticationRequired": "مصادقة SSH مطلوبة", "sshNoKeyboardInteractive": "المصادقة التفاعلية عبر لوحة المفاتيح غير متاحة", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "المصادقة معطلة", "authenticationDisabledDesc": "جميع طرق المصادقة معطلة حاليًا. الرجاء الاتصال بالمسؤول.", "passwordLoginDisabledDesc": "تسجيل الدخول بكلمة المرور معطل. استخدم مفتاح مرور أو مزود مصادقة خارجي.", - "signInWithPasskey": "تسجيل الدخول باستخدام مفتاح المرور", - "passkeyLoginFailed": "فشل تسجيل الدخول بمفتاح المرور", "attemptsRemaining": "{{count}} محاولات متبقية", "confirmResetDataWipe": "لم يتم تسجيل الدخول إلى هذا الحساب منذ ترقية التشفير، لذا لا يمكن استرداد البيانات المخزنة بدون كلمة المرور القديمة. ستؤدي إعادة التعيين إلى حذف المضيفات وبيانات الاعتماد والمقتطفات بشكل دائم. هل تريد المتابعة؟" }, @@ -2065,7 +2061,6 @@ "saving": "جارٍ الحفظ...", "updateRegistrationFailed": "فشل تحديث إعداد التسجيل", "updatePasswordLoginFailed": "فشل تحديث إعداد تسجيل الدخول بكلمة المرور", - "cannotDisablePasswordLoginWithTotp": "لا يمكن تعطيل تسجيل الدخول بكلمة المرور أثناء تمكين المصادقة الثنائية (2FA) لمستخدم واحد أو أكثر. قم بتعطيل المصادقة الثنائية أولاً.", "updateOidcAutoProvisionFailed": "فشل تحديث إعداد التزويد التلقائي لـ OIDC", "updateOidcSilentLoginDefaultFailed": "فشل تحديث إعداد تسجيل الدخول الصامت لـ OIDC", "updatePasswordResetFailed": "فشل تحديث إعداد إعادة تعيين كلمة المرور", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "تمت إعادة تعيين كلمة المرور. تم حذف البيانات المشفرة للمستخدم.", "resetPasswordFailed": "فشلت إعادة تعيين كلمة المرور", "resetPasswordConfirmWipe": "لم يسجل {{username}} الدخول منذ ترقية التشفير، لذا لا يمكن استرداد بياناته. ستؤدي إعادة التعيين الآن إلى حذف مضيفيه وبيانات اعتماده ومقاطع. هل تريد المتابعة؟", - "totpSectionTitle": "المصادقة الثنائية", - "totpStatusEnabled": "تم تمكين TOTP لهذا المستخدم", - "totpStatusDisabled": "لم يتم تمكين TOTP لهذا المستخدم", - "disableTotp": "تعطيل", - "disableTotpConfirm": "تعطيل المصادقة الثنائية لـ {{username}}؟ سيكون بمقدورهم تسجيل الدخول باستخدام كلمة المرور فقط.", - "totpDisabledSuccess": "تم تعطيل TOTP", - "totpDisableFailed": "فشل تعطيل TOTP", "manageApiKeys": "مفاتيح API", "apiKeyNamePlaceholder": "اسم المفتاح", "apiKeyCopyNotice": "انسخ هذا المفتاح الآن، فلن يظهر مرة أخرى.", @@ -2576,23 +2564,6 @@ "settingsUpdates": "التحديثات", "disableUpdateChecks": "تعطيل التحقق من التحديثات", "disableUpdateChecksDesc": "إيقاف التحقق من التحديثات", - "totpAuthenticator": "مُوثِّق TOTP", - "totpEnabled": "التحقق بخطوتين ممكّن", - "totpDisabled": "إضافة أمان إضافي لتسجيل الدخول", - "totpAddAuthenticator": "إضافة جهاز", - "totpAddTitle": "إضافة جهاز المصادقة", - "totpAddDescription": "قم بتأكيد هويتك لتسجيل مصادق آخر باستخدام نفس مفتاح TOTP الآمن.", - "totpAddInputRequired": "أدخل رمز TOTP أو كلمة المرور الخاصة بك", - "totpAddFailed": "فشل إضافة المصادقة", - "totpAddScanInstructions": "امسح رمز الاستجابة السريعة هذا باستخدام أداة المصادقة الإضافية. ستستمر أدوات المصادقة الحالية في العمل.", - "totpAddSuccess": "تم تسجيل مصادق إضافي", - "disable": "تعطيل", - "enable": "تمكين", - "setupTotp": "إعداد TOTP", - "qrCode": "رمز الاستجابة السريعة", - "totpInstructions": "امسح رمز الاستجابة السريعة أو أدخل السر في تطبيق الموثق، ثم أدخل الرمز المكون من 6 أرقام", - "totpCodePlaceholder": "000000", - "verify": "تحقق", "changePassword": "تغيير كلمة المرور", "currentPasswordLabel": "كلمة المرور الحالية", "currentPasswordPlaceholder": "كلمة المرور الحالية", @@ -2638,34 +2609,7 @@ "roleUser": "مستخدم", "authMethodDual": "مصادقة مزدوجة", "authMethodOidc": "OIDC", - "totpSetupFailed": "فشل بدء إعداد TOTP", - "totpEnter6Digits": "أدخل رمزًا من 6 أرقام", - "totpEnabledSuccess": "تم تمكين المصادقة الثنائية", - "totpInvalidCode": "رمز غير صالح، يرجى المحاولة مرة أخرى", - "totpDisableInputRequired": "أدخل رمز TOTP أو كلمة المرور", - "totpDisabledSuccess": "تم تعطيل المصادقة الثنائية", - "totpDisableFailed": "فشل تعطيل المصادقة الثنائية", - "totpDisableTitle": "تعطيل المصادقة الثنائية", - "totpDisablePlaceholder": "أدخل رمز TOTP أو كلمة المرور", - "totpDisableConfirm": "تعطيل المصادقة الثنائية", - "totpContinueVerify": "متابعة للتحقق", - "totpVerifyTitle": "التحقق من الرمز", - "totpBackupTitle": "رموز النسخ الاحتياطي", - "totpDownloadBackup": "تنزيل رموز النسخ الاحتياطي", - "passkeys": "مفاتيح المرور", - "passkeysDesc": "استخدم بيانات اعتماد WebAuthn/FIDO2 لتسجيل الدخول بدون كلمة مرور", - "passkeyName": "اسم مفتاح المرور", - "passkeyUvPreferred": "مفضل", - "passkeyUvRequired": "مطلوب", - "passkeyUvDiscouraged": "غير مشجع", - "addPasskey": "إضافة مفتاح مرور", - "noPasskeys": "لا توجد مفاتيح مرور مسجلة.", - "passkeyAdded": "تمت إضافة مفتاح المرور", - "passkeyAddFailed": "فشل إضافة مفتاح المرور", - "passkeyDeleted": "تم حذف مفتاح المرور", - "passkeyDeleteFailed": "فشل حذف مفتاح المرور", "done": "تم", - "secretCopied": "تم نسخ السر إلى الحافظة", "apiKeyNameRequired": "اسم المفتاح مطلوب", "apiKeyCreated": "تم إنشاء مفتاح API \"{{name}}\"", "apiKeyCreateFailed": "فشل إنشاء مفتاح API", diff --git a/src/ui/locales/translated/bg_BG.json b/src/ui/locales/translated/bg_BG.json index d8360d044..374e8ac34 100644 --- a/src/ui/locales/translated/bg_BG.json +++ b/src/ui/locales/translated/bg_BG.json @@ -1592,8 +1592,6 @@ "hasAccount": "Вече имате акаунт?", "twoFactorAuth": "Двуфакторно удостоверяване", "enterCode": "Въведете код за потвърждение", - "backupCode": "Или използвайте резервен код", - "verifyCode": "Код за проверка", "redirectingToApp": "Пренасочване към приложението...", "sshAuthenticationRequired": "Изисква се SSH удостоверяване", "sshNoKeyboardInteractive": "Интерактивно удостоверяване с клавиатура не е налично", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "Удостоверяването е деактивирано", "authenticationDisabledDesc": "Всички методи за удостоверяване са деактивирани. Моля, свържете се с вашия администратор.", "passwordLoginDisabledDesc": "Входът с парола е деактивиран. Използвайте ключ за достъп или външен доставчик на удостоверяване.", - "signInWithPasskey": "Влезте с парола", - "passkeyLoginFailed": "Влизането с парола не бе успешно", "attemptsRemaining": "{{count}} attempts remaining", "confirmResetDataWipe": "Този акаунт не е влизал след надстройката на криптирането, така че съхранените в него данни не могат да бъдат възстановени без старата парола. Нулирането ще изтрие окончателно неговите хостове, идентификационни данни и фрагменти. Искате ли да продължите?" }, @@ -2065,7 +2061,6 @@ "saving": "Запазване...", "updateRegistrationFailed": "Актуализирането на настройката за регистрация не бе успешно", "updatePasswordLoginFailed": "Актуализирането на настройката за вход с парола не бе успешно", - "cannotDisablePasswordLoginWithTotp": "Не може да се деактивира входът с парола, докато 2FA е активирана за един или повече потребители. Първо деактивирайте 2FA.", "updateOidcAutoProvisionFailed": "Актуализирането на настройката за автоматично предоставяне на OIDC не бе успешно", "updateOidcSilentLoginDefaultFailed": "Актуализирането на настройката за безшумно влизане в OIDC не бе успешно", "updatePasswordResetFailed": "Актуализирането на настройката за нулиране на паролата не бе успешно", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "Нулиране на паролата. Криптираните данни на потребителя бяха изтрити.", "resetPasswordFailed": "Неуспешно нулиране на паролата", "resetPasswordConfirmWipe": "{{username}} не е влизал в системата след надстройката на криптирането, така че данните му не могат да бъдат възстановени. Нулирането сега ще изтрие неговите хостове, идентификационни данни и фрагменти. Искате ли да продължите?", - "totpSectionTitle": "Двуфакторно удостоверяване", - "totpStatusEnabled": "TOTP е активиран за този потребител", - "totpStatusDisabled": "TOTP не е активиран за този потребител", - "disableTotp": "Деактивиране", - "disableTotpConfirm": "Деактивиране на двуфакторното удостоверяване за {{username}}? Те ще могат да влизат само с паролата си.", - "totpDisabledSuccess": "TOTP е деактивиран", - "totpDisableFailed": "Деактивирането на TOTP не бе успешно", "manageApiKeys": "API ключове", "apiKeyNamePlaceholder": "Име на ключа", "apiKeyCopyNotice": "Копирайте този ключ сега, той няма да се показва отново.", @@ -2576,23 +2564,6 @@ "settingsUpdates": "Актуализации", "disableUpdateChecks": "Деактивиране на проверките за актуализации", "disableUpdateChecksDesc": "Спрете проверката за актуализации", - "totpAuthenticator": "TOTP удостоверител", - "totpEnabled": "2FA е активирана", - "totpDisabled": "Добавете допълнителна сигурност за вход", - "totpAddAuthenticator": "Добавяне на устройство", - "totpAddTitle": "Добавяне на устройство за удостоверяване", - "totpAddDescription": "Потвърдете самоличността си, за да регистрирате друг удостоверител със същия защитен TOTP ключ.", - "totpAddInputRequired": "Въведете вашия TOTP код или парола", - "totpAddFailed": "Добавянето на удостоверител не бе успешно", - "totpAddScanInstructions": "Сканирайте този QR код с допълнителния автентификатор. Съществуващите автентификатори ще продължат да работят.", - "totpAddSuccess": "Допълнителен регистриран удостоверител", - "disable": "Деактивиране", - "enable": "Активиране", - "setupTotp": "Настройка на TOTP", - "qrCode": "QR код", - "totpInstructions": "Сканирайте QR кода или въведете секретния код в приложението си за удостоверяване, след което въведете 6-цифрения код", - "totpCodePlaceholder": "000000", - "verify": "Проверка", "changePassword": "Промяна на паролата", "currentPasswordLabel": "Текуща парола", "currentPasswordPlaceholder": "Текуща парола", @@ -2638,34 +2609,7 @@ "roleUser": "Потребител", "authMethodDual": "Двойно удостоверяване", "authMethodOidc": "OIDC", - "totpSetupFailed": "Неуспешно стартиране на настройката на TOTP", - "totpEnter6Digits": "Въведете 6-цифрен код", - "totpEnabledSuccess": "Двуфакторно удостоверяване е активирано", - "totpInvalidCode": "Невалиден код, моля опитайте отново", - "totpDisableInputRequired": "Въведете вашия TOTP код или парола", - "totpDisabledSuccess": "Двуфакторното удостоверяване е деактивирано", - "totpDisableFailed": "Деактивирането на 2FA не бе успешно", - "totpDisableTitle": "Деактивиране на 2FA", - "totpDisablePlaceholder": "Въведете TOTP код или парола", - "totpDisableConfirm": "Деактивиране на 2FA", - "totpContinueVerify": "Продължете с проверката", - "totpVerifyTitle": "Код за проверка", - "totpBackupTitle": "Резервни кодове", - "totpDownloadBackup": "Изтегляне на резервни кодове", - "passkeys": "Пароли", - "passkeysDesc": "Използвайте WebAuthn/FIDO2 идентификационни данни за влизане без парола", - "passkeyName": "Име на паролата", - "passkeyUvPreferred": "Предпочитан", - "passkeyUvRequired": "Задължително", - "passkeyUvDiscouraged": "Обезкуражен", - "addPasskey": "Добавяне на парола", - "noPasskeys": "Няма регистрирани пароли.", - "passkeyAdded": "Добавен е ключ за достъп", - "passkeyAddFailed": "Добавянето на ключ за достъп не бе успешно", - "passkeyDeleted": "Ключът за достъп е изтрит", - "passkeyDeleteFailed": "Изтриването на ключа за достъп не бе успешно", "done": "Готово", - "secretCopied": "Тайната е копирана в клипборда", "apiKeyNameRequired": "Името на ключа е задължително", "apiKeyCreated": "API ключът „{{name}}“ е създаден", "apiKeyCreateFailed": "Създаването на API ключ не бе успешно", diff --git a/src/ui/locales/translated/bn_BD.json b/src/ui/locales/translated/bn_BD.json index e8bcc344c..71cfe9097 100644 --- a/src/ui/locales/translated/bn_BD.json +++ b/src/ui/locales/translated/bn_BD.json @@ -1592,8 +1592,6 @@ "hasAccount": "আপনার কি ইতিমধ্যেই একটি অ্যাকাউন্ট আছে?", "twoFactorAuth": "দ্বি-স্তরীয় প্রমাণীকরণ", "enterCode": "যাচাইকরণ কোড প্রবেশ করান", - "backupCode": "অথবা ব্যাকআপ কোড ব্যবহার করুন", - "verifyCode": "কোড যাচাই করুন", "redirectingToApp": "অ্যাপে পুনঃনির্দেশিত করা হচ্ছে...", "sshAuthenticationRequired": "SSH প্রমাণীকরণ আবশ্যক", "sshNoKeyboardInteractive": "কিবোর্ড-ভিত্তিক প্রমাণীকরণ অনুপলব্ধ", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "প্রমাণীকরণ নিষ্ক্রিয় করা হয়েছে", "authenticationDisabledDesc": "সকল প্রমাণীকরণ পদ্ধতি বর্তমানে নিষ্ক্রিয় রয়েছে। অনুগ্রহ করে আপনার প্রশাসকের সাথে যোগাযোগ করুন।", "passwordLoginDisabledDesc": "পাসওয়ার্ড দিয়ে লগইন করা বন্ধ আছে। একটি পাসকি অথবা বাহ্যিক প্রমাণীকরণ প্রদানকারী ব্যবহার করুন।", - "signInWithPasskey": "পাসকি দিয়ে সাইন ইন করুন", - "passkeyLoginFailed": "পাসকি লগইন ব্যর্থ হয়েছে", "attemptsRemaining": "{{count}} টি প্রচেষ্টা বাকি", "confirmResetDataWipe": "এনক্রিপশন আপগ্রেডের পর থেকে এই অ্যাকাউন্টটি লগ ইন করা হয়নি, তাই পুরোনো পাসওয়ার্ড ছাড়া এর সংরক্ষিত ডেটা পুনরুদ্ধার করা যাবে না। রিসেট করলে এর হোস্ট, ক্রেডেনশিয়াল এবং স্নিপেট স্থায়ীভাবে মুছে যাবে। চালিয়ে যাবেন?" }, @@ -2065,7 +2061,6 @@ "saving": "সঞ্চয়...", "updateRegistrationFailed": "নিবন্ধন সেটিং আপডেট করতে ব্যর্থ হয়েছে", "updatePasswordLoginFailed": "পাসওয়ার্ড লগইন সেটিং আপডেট করতে ব্যর্থ হয়েছে", - "cannotDisablePasswordLoginWithTotp": "এক বা একাধিক ব্যবহারকারীর জন্য 2FA চালু থাকলে পাসওয়ার্ড লগইন নিষ্ক্রিয় করা যাবে না। প্রথমে 2FA নিষ্ক্রিয় করুন।", "updateOidcAutoProvisionFailed": "OIDC অটো-প্রভিশন সেটিং আপডেট করতে ব্যর্থ হয়েছে", "updateOidcSilentLoginDefaultFailed": "সাইলেন্ট OIDC লগইন সেটিং আপডেট করতে ব্যর্থ হয়েছে", "updatePasswordResetFailed": "পাসওয়ার্ড রিসেট সেটিং আপডেট করতে ব্যর্থ হয়েছে", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "পাসওয়ার্ড রিসেট করা হয়েছে। ব্যবহারকারীর এনক্রিপ্টেড ডেটা মুছে ফেলা হয়েছে।", "resetPasswordFailed": "পাসওয়ার্ড রিসেট করতে ব্যর্থ হয়েছে", "resetPasswordConfirmWipe": "এনক্রিপশন আপগ্রেডের পর থেকে {{username}} লগ ইন করেননি, তাই তাদের ডেটা পুনরুদ্ধার করা সম্ভব নয়। এখন রিসেট করলে তাদের হোস্ট, ক্রেডেনশিয়াল এবং স্নিপেট মুছে যাবে। চালিয়ে যাবেন?", - "totpSectionTitle": "দ্বি-স্তরীয় প্রমাণীকরণ", - "totpStatusEnabled": "এই ব্যবহারকারীর জন্য TOTP সক্রিয় করা হয়েছে।", - "totpStatusDisabled": "এই ব্যবহারকারীর জন্য TOTP সক্রিয় করা নেই।", - "disableTotp": "অক্ষম করুন", - "disableTotpConfirm": "{{username}}এর জন্য দ্বি-স্তরীয় প্রমাণীকরণ নিষ্ক্রিয় করবেন? তারা শুধুমাত্র তাদের পাসওয়ার্ড দিয়ে লগ ইন করতে পারবে।", - "totpDisabledSuccess": "TOTP নিষ্ক্রিয়", - "totpDisableFailed": "TOTP নিষ্ক্রিয় করতে ব্যর্থ হয়েছে", "manageApiKeys": "এপিআই কী", "apiKeyNamePlaceholder": "মূল নাম", "apiKeyCopyNotice": "এই কী-টি এখন কপি করুন, এটি আর দেখানো হবে না।", @@ -2576,23 +2564,6 @@ "settingsUpdates": "আপডেট", "disableUpdateChecks": "আপডেট চেক নিষ্ক্রিয় করুন", "disableUpdateChecksDesc": "আপডেট চেক করা বন্ধ করুন", - "totpAuthenticator": "TOTP প্রমাণীকরণকারী", - "totpEnabled": "2FA সক্রিয় করা হয়েছে", - "totpDisabled": "অতিরিক্ত লগইন নিরাপত্তা যোগ করুন", - "totpAddAuthenticator": "ডিভাইস যোগ করুন", - "totpAddTitle": "প্রমাণীকরণ ডিভাইস যোগ করুন", - "totpAddDescription": "একই সুরক্ষিত TOTP কী ব্যবহার করে অন্য একটি প্রমাণীকরণকারী নথিভুক্ত করতে আপনার পরিচয় নিশ্চিত করুন।", - "totpAddInputRequired": "আপনার TOTP কোড বা পাসওয়ার্ড লিখুন", - "totpAddFailed": "প্রমাণীকরণকারী যোগ করতে ব্যর্থ হয়েছে", - "totpAddScanInstructions": "অতিরিক্ত অথেন্টিকেটর দিয়ে এই কিউআর কোডটি স্ক্যান করুন। বিদ্যমান অথেন্টিকেটরগুলো কাজ করতে থাকবে।", - "totpAddSuccess": "অতিরিক্ত প্রমাণীকরণকারী নথিভুক্ত করা হয়েছে", - "disable": "অক্ষম করুন", - "enable": "সক্ষম করুন", - "setupTotp": "TOTP সেটআপ করুন", - "qrCode": "কিউআর কোড", - "totpInstructions": "আপনার অথেন্টিকেটর অ্যাপে কিউআর কোড স্ক্যান করুন অথবা সিক্রেটটি প্রবেশ করান, তারপর ৬-সংখ্যার কোডটি লিখুন।", - "totpCodePlaceholder": "000000", - "verify": "যাচাই করুন", "changePassword": "পাসওয়ার্ড পরিবর্তন করুন", "currentPasswordLabel": "বর্তমান পাসওয়ার্ড", "currentPasswordPlaceholder": "বর্তমান পাসওয়ার্ড", @@ -2638,34 +2609,7 @@ "roleUser": "ব্যবহারকারী", "authMethodDual": "দ্বৈত প্রমাণীকরণ", "authMethodOidc": "ওআইডিসি", - "totpSetupFailed": "TOTP সেটআপ শুরু করতে ব্যর্থ হয়েছে", - "totpEnter6Digits": "একটি ৬-সংখ্যার কোড প্রবেশ করান", - "totpEnabledSuccess": "দ্বি-স্তর প্রমাণীকরণ সক্রিয় করা হয়েছে", - "totpInvalidCode": "কোডটি ভুল, অনুগ্রহ করে আবার চেষ্টা করুন।", - "totpDisableInputRequired": "আপনার TOTP কোড বা পাসওয়ার্ড লিখুন", - "totpDisabledSuccess": "দ্বি-স্তর প্রমাণীকরণ নিষ্ক্রিয় করা হয়েছে", - "totpDisableFailed": "2FA নিষ্ক্রিয় করতে ব্যর্থ হয়েছে", - "totpDisableTitle": "2FA নিষ্ক্রিয় করুন", - "totpDisablePlaceholder": "TOTP কোড বা পাসওয়ার্ড প্রবেশ করান", - "totpDisableConfirm": "2FA নিষ্ক্রিয় করুন", - "totpContinueVerify": "যাচাই চালিয়ে যান", - "totpVerifyTitle": "কোড যাচাই করুন", - "totpBackupTitle": "ব্যাকআপ কোড", - "totpDownloadBackup": "ব্যাকআপ কোড ডাউনলোড করুন", - "passkeys": "পাসকি", - "passkeysDesc": "পাসওয়ার্ড ছাড়া সাইন ইন করতে WebAuthn/FIDO2 ক্রেডেনশিয়াল ব্যবহার করুন।", - "passkeyName": "পাসকি নাম", - "passkeyUvPreferred": "পছন্দের", - "passkeyUvRequired": "প্রয়োজনীয়", - "passkeyUvDiscouraged": "হতাশ", - "addPasskey": "পাসকি যোগ করুন", - "noPasskeys": "কোনো পাসকি নিবন্ধিত নেই।", - "passkeyAdded": "পাসকি যোগ করা হয়েছে", - "passkeyAddFailed": "পাসকি যোগ করতে ব্যর্থ হয়েছে", - "passkeyDeleted": "পাসকি মুছে ফেলা হয়েছে", - "passkeyDeleteFailed": "পাসকি মুছে ফেলতে ব্যর্থ হয়েছে", "done": "সম্পন্ন হয়েছে", - "secretCopied": "গোপনীয় তথ্য ক্লিপবোর্ডে কপি করা হয়েছে", "apiKeyNameRequired": "কী-এর নাম আবশ্যক।", "apiKeyCreated": "এপিআই কী \"{{name}}\" তৈরি করা হয়েছে", "apiKeyCreateFailed": "এপিআই কী তৈরি করতে ব্যর্থ হয়েছে", diff --git a/src/ui/locales/translated/ca_ES.json b/src/ui/locales/translated/ca_ES.json index c3ffa8e3d..211e11a99 100644 --- a/src/ui/locales/translated/ca_ES.json +++ b/src/ui/locales/translated/ca_ES.json @@ -1592,8 +1592,6 @@ "hasAccount": "Ja tens un compte?", "twoFactorAuth": "Autenticació de dos factors", "enterCode": "Introduïu el codi de verificació", - "backupCode": "O bé feu servir un codi de còpia de seguretat", - "verifyCode": "Verifica el codi", "redirectingToApp": "Redireccionant a l'aplicació...", "sshAuthenticationRequired": "Autenticació SSH requerida", "sshNoKeyboardInteractive": "Autenticació interactiva amb teclat no disponible", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "Autenticació desactivada", "authenticationDisabledDesc": "Tots els mètodes d'autenticació estan actualment desactivats. Poseu-vos en contacte amb el vostre administrador.", "passwordLoginDisabledDesc": "L'inici de sessió amb contrasenya està desactivat. Feu servir una contrasenya o un proveïdor d'autenticació extern.", - "signInWithPasskey": "Inicia la sessió amb la clau d'accés", - "passkeyLoginFailed": "Error d'inici de sessió amb contrasenya", "attemptsRemaining": "{{count}} intents restants", "confirmResetDataWipe": "Aquest compte no ha iniciat sessió des de l'actualització del xifratge, de manera que les dades emmagatzemades no es poden recuperar sense la contrasenya antiga. Si el restableixes, se suprimiran permanentment els seus amfitrions, credencials i fragments. Vols continuar?" }, @@ -2065,7 +2061,6 @@ "saving": "Desant...", "updateRegistrationFailed": "No s'ha pogut actualitzar la configuració del registre", "updatePasswordLoginFailed": "No s'ha pogut actualitzar la configuració d'inici de sessió amb contrasenya", - "cannotDisablePasswordLoginWithTotp": "No es pot desactivar l'inici de sessió amb contrasenya mentre l'autenticació a dues fades (2FA) està activada per a un o més usuaris. Primer cal desactivar-la.", "updateOidcAutoProvisionFailed": "No s'ha pogut actualitzar la configuració d'aprovisionament automàtic d'OIDC.", "updateOidcSilentLoginDefaultFailed": "No s'ha pogut actualitzar la configuració d'inici de sessió silenciós de l'OIDC.", "updatePasswordResetFailed": "No s'ha pogut actualitzar la configuració de restabliment de la contrasenya", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "Restabliment de la contrasenya. S'han esborrat les dades xifrades de l'usuari.", "resetPasswordFailed": "No s'ha pogut restablir la contrasenya", "resetPasswordConfirmWipe": "{{username}} no ha iniciat sessió des de l'actualització del xifratge, de manera que les seves dades no es poden recuperar. Si restableixes ara, se suprimiran els seus amfitrions, credencials i fragments. Vols continuar?", - "totpSectionTitle": "Autenticació de dos factors", - "totpStatusEnabled": "TOTP està habilitat per a aquest usuari", - "totpStatusDisabled": "El TOTP no està habilitat per a aquest usuari", - "disableTotp": "Desactiva", - "disableTotpConfirm": "Voleu desactivar l'autenticació de dos factors per a {{username}}? Només podran iniciar sessió amb la seva contrasenya.", - "totpDisabledSuccess": "TOTP desactivat", - "totpDisableFailed": "No s'ha pogut desactivar TOTP", "manageApiKeys": "Claus API", "apiKeyNamePlaceholder": "Nom de la clau", "apiKeyCopyNotice": "Copia aquesta clau ara, no es tornarà a mostrar.", @@ -2576,23 +2564,6 @@ "settingsUpdates": "Actualitzacions", "disableUpdateChecks": "Desactiva les comprovacions d'actualització", "disableUpdateChecksDesc": "Deixa de buscar actualitzacions", - "totpAuthenticator": "Autenticador TOTP", - "totpEnabled": "2FA està activat", - "totpDisabled": "Afegeix seguretat d'inici de sessió addicional", - "totpAddAuthenticator": "Afegeix un dispositiu", - "totpAddTitle": "Afegeix un dispositiu d'autenticació", - "totpAddDescription": "Confirma la teva identitat per inscriure un altre autenticador amb la mateixa clau TOTP segura.", - "totpAddInputRequired": "Introdueix el teu codi TOTP o contrasenya", - "totpAddFailed": "No s'ha pogut afegir l'autenticador", - "totpAddScanInstructions": "Escaneja aquest codi QR amb l'autenticador addicional. Els autenticadors existents continuaran funcionant.", - "totpAddSuccess": "Autenticador addicional inscrit", - "disable": "Desactiva", - "enable": "Activa", - "setupTotp": "Configura TOTP", - "qrCode": "Codi QR", - "totpInstructions": "Escaneja el codi QR o introdueix el secret a l'aplicació d'autenticació i, a continuació, introdueix el codi de 6 dígits.", - "totpCodePlaceholder": "000000", - "verify": "Verifica", "changePassword": "Canvia la contrasenya", "currentPasswordLabel": "Contrasenya actual", "currentPasswordPlaceholder": "Contrasenya actual", @@ -2638,34 +2609,7 @@ "roleUser": "Usuari", "authMethodDual": "Doble autenticació", "authMethodOidc": "OIDC", - "totpSetupFailed": "No s'ha pogut iniciar la configuració del TOTP", - "totpEnter6Digits": "Introduïu un codi de 6 dígits", - "totpEnabledSuccess": "Autenticació de dos factors activada", - "totpInvalidCode": "Codi no vàlid, si us plau, torna-ho a intentar.", - "totpDisableInputRequired": "Introdueix el teu codi TOTP o contrasenya", - "totpDisabledSuccess": "Autenticació de dos factors desactivada", - "totpDisableFailed": "No s'ha pogut desactivar la 2FA", - "totpDisableTitle": "Desactiva la 2FA", - "totpDisablePlaceholder": "Introduïu el codi TOTP o la contrasenya", - "totpDisableConfirm": "Desactiva la 2FA", - "totpContinueVerify": "Continua a verificar", - "totpVerifyTitle": "Verifica el codi", - "totpBackupTitle": "Codis de còpia de seguretat", - "totpDownloadBackup": "Baixa els codis de còpia de seguretat", - "passkeys": "Claus d'accés", - "passkeysDesc": "Utilitzeu les credencials de WebAuthn/FIDO2 per iniciar la sessió sense contrasenya", - "passkeyName": "Nom de la clau d'accés", - "passkeyUvPreferred": "Preferit", - "passkeyUvRequired": "Obligatori", - "passkeyUvDiscouraged": "Desanimat", - "addPasskey": "Afegeix una contrasenya", - "noPasskeys": "No hi ha cap clau d'accés registrada.", - "passkeyAdded": "Clau d'accés afegida", - "passkeyAddFailed": "No s'ha pogut afegir la contrasenya", - "passkeyDeleted": "Clau d'accés suprimida", - "passkeyDeleteFailed": "No s'ha pogut suprimir la clau d'accés", "done": "Fet", - "secretCopied": "Secret copiat al porta-retalls", "apiKeyNameRequired": "El nom de la clau és obligatori", "apiKeyCreated": "Clau API \"{{name}}\" creada", "apiKeyCreateFailed": "No s'ha pogut crear la clau de l'API", diff --git a/src/ui/locales/translated/cs_CZ.json b/src/ui/locales/translated/cs_CZ.json index 8f2d20bec..e4929c5d7 100644 --- a/src/ui/locales/translated/cs_CZ.json +++ b/src/ui/locales/translated/cs_CZ.json @@ -1592,8 +1592,6 @@ "hasAccount": "Už máte účet?", "twoFactorAuth": "Dvoufaktorové ověřování", "enterCode": "Zadejte ověřovací kód", - "backupCode": "Nebo použijte záložní kód", - "verifyCode": "Ověřte kód", "redirectingToApp": "Přesměrování do aplikace...", "sshAuthenticationRequired": "Vyžadováno ověřování SSH", "sshNoKeyboardInteractive": "Interaktivní ověřování pomocí klávesnice není k dispozici", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "Ověřování zakázáno", "authenticationDisabledDesc": "Všechny metody ověřování jsou momentálně zakázány. Kontaktujte prosím svého administrátora.", "passwordLoginDisabledDesc": "Přihlášení heslem je zakázáno. Použijte přístupový klíč nebo externího poskytovatele ověřování.", - "signInWithPasskey": "Přihlásit se pomocí hesla", - "passkeyLoginFailed": "Přihlášení k heslu se nezdařilo", "attemptsRemaining": "Zbývá {{count}} pokusů", "confirmResetDataWipe": "Tento účet se od aktualizace šifrování nepřihlásil, takže jeho uložená data nelze obnovit bez starého hesla. Resetováním se trvale smažou jeho hostitelé, přihlašovací údaje a úryvky. Pokračovat?" }, @@ -2065,7 +2061,6 @@ "saving": "Ukládání...", "updateRegistrationFailed": "Nepodařilo se aktualizovat nastavení registrace", "updatePasswordLoginFailed": "Nepodařilo se aktualizovat nastavení přihlašovacího hesla", - "cannotDisablePasswordLoginWithTotp": "Nelze zakázat přihlašování heslem, pokud je pro jednoho nebo více uživatelů povoleno 2FA. Nejprve 2FA zakažte.", "updateOidcAutoProvisionFailed": "Nepodařilo se aktualizovat nastavení automatického zřizování OIDC.", "updateOidcSilentLoginDefaultFailed": "Nepodařilo se aktualizovat nastavení tichého přihlášení OIDC.", "updatePasswordResetFailed": "Nastavení resetování hesla se nepodařilo aktualizovat", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "Reset hesla. Šifrovaná data uživatele byla vymazána.", "resetPasswordFailed": "Obnovení hesla se nezdařilo", "resetPasswordConfirmWipe": "{{username}} se od aktualizace šifrování nepřihlásil, takže jeho data nelze obnovit. Resetováním nyní smažete jeho hostitele, přihlašovací údaje a úryvky. Pokračovat?", - "totpSectionTitle": "Dvoufaktorové ověřování", - "totpStatusEnabled": "TOTP je pro tohoto uživatele povolen", - "totpStatusDisabled": "TOTP není pro tohoto uživatele povoleno", - "disableTotp": "Zakázat", - "disableTotpConfirm": "Zakázat dvoufaktorové ověřování pro {{username}}? Budou se moci přihlásit pouze pomocí hesla.", - "totpDisabledSuccess": "TOTP zakázán", - "totpDisableFailed": "Nepodařilo se zakázat TOTP", "manageApiKeys": "Klíče API", "apiKeyNamePlaceholder": "Název klíče", "apiKeyCopyNotice": "Zkopírujte tento klíč nyní, už se vám nezobrazí.", @@ -2576,23 +2564,6 @@ "settingsUpdates": "Aktualizace", "disableUpdateChecks": "Zakázat kontroly aktualizací", "disableUpdateChecksDesc": "Zastavit kontrolu aktualizací", - "totpAuthenticator": "Ověřovač TOTP", - "totpEnabled": "2FA je povoleno", - "totpDisabled": "Přidejte další zabezpečení přihlášení", - "totpAddAuthenticator": "Přidat zařízení", - "totpAddTitle": "Přidat ověřovací zařízení", - "totpAddDescription": "Potvrďte svou identitu a zaregistrujte dalšího ověřovatele se stejným zabezpečeným klíčem TOTP.", - "totpAddInputRequired": "Zadejte svůj kód TOTP nebo heslo", - "totpAddFailed": "Přidání ověřovatele se nezdařilo", - "totpAddScanInstructions": "Naskenujte tento QR kód pomocí dalšího ověřovače. Stávající ověřovače budou i nadále fungovat.", - "totpAddSuccess": "Další ověřovatel zaregistrován", - "disable": "Zakázat", - "enable": "Umožnit", - "setupTotp": "Nastavení TOTP", - "qrCode": "QR kód", - "totpInstructions": "Naskenujte QR kód nebo zadejte tajný kód do ověřovací aplikace a poté zadejte 6místný kód.", - "totpCodePlaceholder": "000000", - "verify": "Ověřit", "changePassword": "Změnit heslo", "currentPasswordLabel": "Aktuální heslo", "currentPasswordPlaceholder": "Aktuální heslo", @@ -2638,34 +2609,7 @@ "roleUser": "Uživatel", "authMethodDual": "Duální ověřování", "authMethodOidc": "OIDC", - "totpSetupFailed": "Nepodařilo se spustit nastavení TOTP", - "totpEnter6Digits": "Zadejte 6místný kód", - "totpEnabledSuccess": "Dvoufázové ověřování povoleno", - "totpInvalidCode": "Neplatný kód, zkuste to prosím znovu.", - "totpDisableInputRequired": "Zadejte svůj kód TOTP nebo heslo", - "totpDisabledSuccess": "Dvoufázové ověřování zakázáno", - "totpDisableFailed": "Nepodařilo se zakázat 2FA", - "totpDisableTitle": "Zakázat 2FA", - "totpDisablePlaceholder": "Zadejte kód TOTP nebo heslo", - "totpDisableConfirm": "Zakázat 2FA", - "totpContinueVerify": "Pokračovat k ověřování", - "totpVerifyTitle": "Ověřte kód", - "totpBackupTitle": "Záložní kódy", - "totpDownloadBackup": "Stáhnout záložní kódy", - "passkeys": "Přístupové klíče", - "passkeysDesc": "Pro přihlášení bez hesla použijte přihlašovací údaje WebAuthn/FIDO2", - "passkeyName": "Název přístupového klíče", - "passkeyUvPreferred": "Preferované", - "passkeyUvRequired": "Požadovaný", - "passkeyUvDiscouraged": "Odradit", - "addPasskey": "Přidat přístupový klíč", - "noPasskeys": "Nejsou registrovány žádné přístupové klíče.", - "passkeyAdded": "Přístupový klíč byl přidán", - "passkeyAddFailed": "Nepodařilo se přidat přístupový klíč", - "passkeyDeleted": "Přístupový klíč smazán", - "passkeyDeleteFailed": "Nepodařilo se smazat přístupový klíč", "done": "Hotovo", - "secretCopied": "Tajný kód zkopírován do schránky", "apiKeyNameRequired": "Název klíče je povinný", "apiKeyCreated": "Klíč API „{{name}}“ byl vytvořen", "apiKeyCreateFailed": "Nepodařilo se vytvořit klíč API", diff --git a/src/ui/locales/translated/da_DK.json b/src/ui/locales/translated/da_DK.json index 62d953962..17e70d2ec 100644 --- a/src/ui/locales/translated/da_DK.json +++ b/src/ui/locales/translated/da_DK.json @@ -1592,8 +1592,6 @@ "hasAccount": "Har du allerede en konto?", "twoFactorAuth": "Tofaktorgodkendelse", "enterCode": "Indtast bekræftelseskode", - "backupCode": "Eller brug backupkoden", - "verifyCode": "Bekræft kode", "redirectingToApp": "Omdirigerer til app...", "sshAuthenticationRequired": "SSH-godkendelse påkrævet", "sshNoKeyboardInteractive": "Tastaturinteraktiv godkendelse ikke tilgængelig", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "Godkendelse deaktiveret", "authenticationDisabledDesc": "Alle godkendelsesmetoder er i øjeblikket deaktiveret. Kontakt din administrator.", "passwordLoginDisabledDesc": "Adgangskodelogin er deaktiveret. Brug en adgangsnøgle eller en ekstern godkendelsesudbyder.", - "signInWithPasskey": "Log ind med adgangsnøgle", - "passkeyLoginFailed": "Login med adgangsnøgle mislykkedes", "attemptsRemaining": "{{count}} forsøg tilbage", "confirmResetDataWipe": "Denne konto er ikke logget ind siden krypteringsopgraderingen, så dens gemte data kan ikke gendannes uden den gamle adgangskode. Nulstilling vil permanent slette dens værter, legitimationsoplysninger og kodestykker. Vil du fortsætte?" }, @@ -2065,7 +2061,6 @@ "saving": "Gemmer...", "updateRegistrationFailed": "Kunne ikke opdatere registreringsindstillingerne", "updatePasswordLoginFailed": "Kunne ikke opdatere adgangskodeindstillingen for login", - "cannotDisablePasswordLoginWithTotp": "Kan ikke deaktivere adgangskodelogin, mens 2FA er aktiveret for en eller flere brugere. Deaktiver 2FA først.", "updateOidcAutoProvisionFailed": "Kunne ikke opdatere indstillingen for automatisk provisionering af OIDC", "updateOidcSilentLoginDefaultFailed": "Kunne ikke opdatere indstillingen for lydløs OIDC-login", "updatePasswordResetFailed": "Indstillingen for nulstilling af adgangskode kunne ikke opdateres", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "Nulstilling af adgangskode. Brugerens krypterede data blev slettet.", "resetPasswordFailed": "Kunne ikke nulstille adgangskoden", "resetPasswordConfirmWipe": "{{username}} har ikke logget ind siden krypteringsopgraderingen, så deres data kan ikke gendannes. Nulstilling nu vil slette deres værter, legitimationsoplysninger og kodestykker. Vil du fortsætte?", - "totpSectionTitle": "Tofaktorgodkendelse", - "totpStatusEnabled": "TOTP er aktiveret for denne bruger", - "totpStatusDisabled": "TOTP er ikke aktiveret for denne bruger", - "disableTotp": "Deaktiver", - "disableTotpConfirm": "Deaktiver tofaktorgodkendelse for {{username}}? De vil kun kunne logge ind med deres adgangskode.", - "totpDisabledSuccess": "TOTP deaktiveret", - "totpDisableFailed": "Kunne ikke deaktivere TOTP", "manageApiKeys": "API-nøgler", "apiKeyNamePlaceholder": "Nøglenavn", "apiKeyCopyNotice": "Kopier denne nøgle nu, den vil ikke blive vist igen.", @@ -2576,23 +2564,6 @@ "settingsUpdates": "Opdateringer", "disableUpdateChecks": "Deaktiver opdateringstjek", "disableUpdateChecksDesc": "Stop med at tjekke for opdateringer", - "totpAuthenticator": "TOTP-godkendelse", - "totpEnabled": "2FA er aktiveret", - "totpDisabled": "Tilføj ekstra loginsikkerhed", - "totpAddAuthenticator": "Tilføj enhed", - "totpAddTitle": "Tilføj godkendelsesenhed", - "totpAddDescription": "Bekræft din identitet for at tilmelde en anden autentificator med den samme sikre TOTP-nøgle.", - "totpAddInputRequired": "Indtast din TOTP-kode eller adgangskode", - "totpAddFailed": "Kunne ikke tilføje godkendelse", - "totpAddScanInstructions": "Scan denne QR-kode med den ekstra godkendelsesfunktion. Eksisterende godkendelsesfunktioner vil fortsat virke.", - "totpAddSuccess": "Yderligere godkendelsesfunktion tilmeldt", - "disable": "Deaktiver", - "enable": "Aktivér", - "setupTotp": "Opsæt TOTP", - "qrCode": "QR-kode", - "totpInstructions": "Scan QR-koden eller indtast hemmeligheden i din autentificeringsapp, og indtast derefter den 6-cifrede kode", - "totpCodePlaceholder": "000000", - "verify": "Verificere", "changePassword": "Skift adgangskode", "currentPasswordLabel": "Nuværende adgangskode", "currentPasswordPlaceholder": "Nuværende adgangskode", @@ -2638,34 +2609,7 @@ "roleUser": "Bruger", "authMethodDual": "Dobbeltgodkendelse", "authMethodOidc": "OIDC", - "totpSetupFailed": "Kunne ikke starte TOTP-opsætningen", - "totpEnter6Digits": "Indtast en 6-cifret kode", - "totpEnabledSuccess": "Tofaktorgodkendelse aktiveret", - "totpInvalidCode": "Ugyldig kode, prøv igen", - "totpDisableInputRequired": "Indtast din TOTP-kode eller adgangskode", - "totpDisabledSuccess": "Tofaktorgodkendelse deaktiveret", - "totpDisableFailed": "Kunne ikke deaktivere 2FA", - "totpDisableTitle": "Deaktiver 2FA", - "totpDisablePlaceholder": "Indtast TOTP-kode eller adgangskode", - "totpDisableConfirm": "Deaktiver 2FA", - "totpContinueVerify": "Fortsæt med at bekræfte", - "totpVerifyTitle": "Bekræft kode", - "totpBackupTitle": "Backupkoder", - "totpDownloadBackup": "Download backupkoder", - "passkeys": "Adgangsnøgler", - "passkeysDesc": "Brug WebAuthn/FIDO2-legitimationsoplysninger til at logge ind uden en adgangskode", - "passkeyName": "Adgangsnøglenavn", - "passkeyUvPreferred": "Foretrukket", - "passkeyUvRequired": "Påkrævet", - "passkeyUvDiscouraged": "Modløs", - "addPasskey": "Tilføj adgangsnøgle", - "noPasskeys": "Ingen adgangsnøgler registreret.", - "passkeyAdded": "Adgangsnøgle tilføjet", - "passkeyAddFailed": "Kunne ikke tilføje adgangsnøgle", - "passkeyDeleted": "Adgangsnøgle slettet", - "passkeyDeleteFailed": "Kunne ikke slette adgangsnøglen", "done": "Færdig", - "secretCopied": "Hemmelighed kopieret til udklipsholder", "apiKeyNameRequired": "Nøglenavn er påkrævet", "apiKeyCreated": "API-nøglen \"{{name}}\" blev oprettet", "apiKeyCreateFailed": "Kunne ikke oprette API-nøgle", diff --git a/src/ui/locales/translated/de_DE.json b/src/ui/locales/translated/de_DE.json index 441035479..c1eb5c342 100644 --- a/src/ui/locales/translated/de_DE.json +++ b/src/ui/locales/translated/de_DE.json @@ -1592,8 +1592,6 @@ "hasAccount": "Bereits ein Konto?", "twoFactorAuth": "Zwei-Faktor-Authentifizierung", "enterCode": "Bestätigungscode eingeben", - "backupCode": "Oder Backup-Code verwenden", - "verifyCode": "Code bestätigen", "redirectingToApp": "Weiterleitung zur App...", "sshAuthenticationRequired": "SSH-Authentifizierung erforderlich", "sshNoKeyboardInteractive": "Keyboard-Interactive-Authentifizierung nicht verfügbar", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "Authentifizierung deaktiviert", "authenticationDisabledDesc": "Alle Authentifizierungsmethoden sind derzeit deaktiviert. Bitte wenden Sie sich an Ihren Administrator.", "passwordLoginDisabledDesc": "Passwort-Anmeldung ist deaktiviert. Verwenden Sie einen Passkey oder einen externen Authentifizierungsanbieter.", - "signInWithPasskey": "Mit Passkey anmelden", - "passkeyLoginFailed": "Passkey-Anmeldung fehlgeschlagen", "attemptsRemaining": "{{count}} Versuche übrig", "confirmResetDataWipe": "Dieses Konto hat sich seit dem Verschlüsselungs-Upgrade nicht angemeldet, daher können die gespeicherten Daten ohne das alte Passwort nicht wiederhergestellt werden. Ein Zurücksetzen löscht Hosts, Anmeldedaten und Snippets endgültig. Fortfahren?" }, @@ -2065,7 +2061,6 @@ "saving": "Speichern...", "updateRegistrationFailed": "Fehler beim Aktualisieren der Registrierungseinstellung", "updatePasswordLoginFailed": "Fehler beim Aktualisieren der Passwort-Anmeldeeinstellung", - "cannotDisablePasswordLoginWithTotp": "Passwort-Anmeldung kann nicht deaktiviert werden, solange 2FA für einen oder mehrere Benutzer aktiviert ist. Deaktivieren Sie zuerst 2FA.", "updateOidcAutoProvisionFailed": "Fehler beim Aktualisieren der OIDC-Autoprovisionierungseinstellung", "updateOidcSilentLoginDefaultFailed": "Fehler beim Aktualisieren der Einstellung für stille OIDC-Anmeldung", "updatePasswordResetFailed": "Fehler beim Aktualisieren der Passwort-Zurücksetzen-Einstellung", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "Passwort zurückgesetzt. Die verschlüsselten Daten des Benutzers wurden gelöscht.", "resetPasswordFailed": "Passwort zurücksetzen fehlgeschlagen", "resetPasswordConfirmWipe": "{{username}} hat sich seit dem Verschlüsselungs-Upgrade nicht angemeldet, daher können die Daten nicht wiederhergestellt werden. Durch das Zurücksetzen werden die Hosts, Zugangsdaten und Snippets gelöscht. Fortfahren?", - "totpSectionTitle": "Zwei-Faktor-Authentifizierung", - "totpStatusEnabled": "TOTP ist für diesen Benutzer aktiviert", - "totpStatusDisabled": "TOTP ist für diesen Benutzer nicht aktiviert", - "disableTotp": "Deaktivieren", - "disableTotpConfirm": "Zwei-Faktor-Authentifizierung für {{username}} deaktivieren? Die Anmeldung ist dann nur mit dem Passwort möglich.", - "totpDisabledSuccess": "TOTP deaktiviert", - "totpDisableFailed": "TOTP deaktivieren fehlgeschlagen", "manageApiKeys": "API-Schlüssel", "apiKeyNamePlaceholder": "Schlüsselname", "apiKeyCopyNotice": "Kopieren Sie diesen Schlüssel jetzt. Er wird nicht erneut angezeigt.", @@ -2576,23 +2564,6 @@ "settingsUpdates": "Updates", "disableUpdateChecks": "Update-Prüfungen deaktivieren", "disableUpdateChecksDesc": "Nicht mehr nach Updates suchen", - "totpAuthenticator": "TOTP-Authentifikator", - "totpEnabled": "2FA ist aktiviert", - "totpDisabled": "Zusätzliche Anmeldesicherheit hinzufügen", - "totpAddAuthenticator": "Gerät hinzufügen", - "totpAddTitle": "Authentifizierungsgerät hinzufügen", - "totpAddDescription": "Bestätigen Sie Ihre Identität, um einen weiteren Authentifikator mit demselben sicheren TOTP-Schlüssel zu registrieren.", - "totpAddInputRequired": "Geben Sie Ihren TOTP-Code oder Ihr Passwort ein.", - "totpAddFailed": "Authentifikator konnte nicht hinzugefügt werden", - "totpAddScanInstructions": "Scannen Sie diesen QR-Code mit dem zusätzlichen Authentifikator. Bestehende Authentifikatoren funktionieren weiterhin.", - "totpAddSuccess": "Zusätzlicher Authentifizierungsalgorithmus registriert", - "disable": "Deaktivieren", - "enable": "Aktivieren", - "setupTotp": "TOTP einrichten", - "qrCode": "QR-Code", - "totpInstructions": "Scannen Sie den QR-Code oder geben Sie den geheimen Schlüssel in Ihrer Authentifizierungs-App ein und geben Sie dann den 6-stelligen Code ein.", - "totpCodePlaceholder": "000000", - "verify": "Verifizieren", "changePassword": "Passwort ändern", "currentPasswordLabel": "Aktuelles Passwort", "currentPasswordPlaceholder": "Aktuelles Passwort", @@ -2638,34 +2609,7 @@ "roleUser": "Benutzer", "authMethodDual": "Duale Authentifizierung", "authMethodOidc": "OIDC", - "totpSetupFailed": "Fehler beim Starten der TOTP-Einrichtung", - "totpEnter6Digits": "Geben Sie einen 6-stelligen Code ein", - "totpEnabledSuccess": "Zwei-Faktor-Authentifizierung aktiviert", - "totpInvalidCode": "Ungültiger Code, bitte versuchen Sie es erneut", - "totpDisableInputRequired": "Geben Sie Ihren TOTP-Code oder Ihr Passwort ein", - "totpDisabledSuccess": "Zwei-Faktor-Authentifizierung deaktiviert", - "totpDisableFailed": "Fehler beim Deaktivieren der 2FA", - "totpDisableTitle": "2FA deaktivieren", - "totpDisablePlaceholder": "TOTP-Code oder Passwort eingeben", - "totpDisableConfirm": "2FA deaktivieren", - "totpContinueVerify": "Weiter zur Verifizierung", - "totpVerifyTitle": "Code verifizieren", - "totpBackupTitle": "Backup-Codes", - "totpDownloadBackup": "Backup-Codes herunterladen", - "passkeys": "Passkeys", - "passkeysDesc": "WebAuthn/FIDO2-Anmeldedaten für eine Anmeldung ohne Passwort verwenden", - "passkeyName": "Passkey-Name", - "passkeyUvPreferred": "Bevorzugt", - "passkeyUvRequired": "Erforderlich", - "passkeyUvDiscouraged": "Nicht empfohlen", - "addPasskey": "Passkey hinzufügen", - "noPasskeys": "Keine Passkeys registriert.", - "passkeyAdded": "Passkey hinzugefügt", - "passkeyAddFailed": "Fehler beim Hinzufügen des Passkeys", - "passkeyDeleted": "Passkey gelöscht", - "passkeyDeleteFailed": "Löschen des Passkeys fehlgeschlagen", "done": "Fertig", - "secretCopied": "Secret in die Zwischenablage kopiert", "apiKeyNameRequired": "Schlüsselname ist erforderlich", "apiKeyCreated": "API-Schlüssel \"{{name}}\" erstellt", "apiKeyCreateFailed": "Erstellen des API-Schlüssels fehlgeschlagen", diff --git a/src/ui/locales/translated/el_GR.json b/src/ui/locales/translated/el_GR.json index 6ff770451..7dbaf3f0a 100644 --- a/src/ui/locales/translated/el_GR.json +++ b/src/ui/locales/translated/el_GR.json @@ -1592,8 +1592,6 @@ "hasAccount": "Έχετε ήδη λογαριασμό;", "twoFactorAuth": "Έλεγχος ταυτότητας δύο παραγόντων", "enterCode": "Εισαγάγετε τον κωδικό επαλήθευσης", - "backupCode": "Ή χρησιμοποιήστε εφεδρικό κωδικό", - "verifyCode": "Επαλήθευση Κωδικού", "redirectingToApp": "Ανακατεύθυνση στην εφαρμογή...", "sshAuthenticationRequired": "Απαιτείται έλεγχος ταυτότητας SSH", "sshNoKeyboardInteractive": "Ο έλεγχος ταυτότητας μέσω διαδραστικού πληκτρολογίου δεν είναι διαθέσιμος", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "Απενεργοποιήθηκε ο έλεγχος ταυτότητας", "authenticationDisabledDesc": "Όλες οι μέθοδοι ελέγχου ταυτότητας είναι αυτήν τη στιγμή απενεργοποιημένες. Επικοινωνήστε με τον διαχειριστή σας.", "passwordLoginDisabledDesc": "Η σύνδεση με κωδικό πρόσβασης είναι απενεργοποιημένη. Χρησιμοποιήστε κλειδί πρόσβασης ή εξωτερικό πάροχο ελέγχου ταυτότητας.", - "signInWithPasskey": "Σύνδεση με κλειδί πρόσβασης", - "passkeyLoginFailed": "Η σύνδεση με κωδικό πρόσβασης απέτυχε", "attemptsRemaining": "{{count}} προσπάθειες απομένουν", "confirmResetDataWipe": "Αυτός ο λογαριασμός δεν έχει συνδεθεί από την αναβάθμιση κρυπτογράφησης, επομένως τα αποθηκευμένα δεδομένα του δεν μπορούν να ανακτηθούν χωρίς τον παλιό κωδικό πρόσβασης. Η επαναφορά θα διαγράψει οριστικά τους κεντρικούς υπολογιστές, τα διαπιστευτήρια και τα αποσπάσματα κώδικα. Θέλετε να συνεχίσετε;" }, @@ -2065,7 +2061,6 @@ "saving": "Οικονομία...", "updateRegistrationFailed": "Η ενημέρωση της ρύθμισης εγγραφής απέτυχε", "updatePasswordLoginFailed": "Αποτυχία ενημέρωσης ρύθμισης κωδικού πρόσβασης σύνδεσης", - "cannotDisablePasswordLoginWithTotp": "Δεν είναι δυνατή η απενεργοποίηση της σύνδεσης με κωδικό πρόσβασης ενώ είναι ενεργοποιημένη η δυνατότητα 2FA για έναν ή περισσότερους χρήστες. Απενεργοποιήστε πρώτα την δυνατότητα 2FA.", "updateOidcAutoProvisionFailed": "Αποτυχία ενημέρωσης της ρύθμισης αυτόματης παροχής OIDC", "updateOidcSilentLoginDefaultFailed": "Αποτυχία ενημέρωσης ρύθμισης αθόρυβης σύνδεσης OIDC", "updatePasswordResetFailed": "Η ενημέρωση της ρύθμισης επαναφοράς κωδικού πρόσβασης απέτυχε", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "Επαναφορά κωδικού πρόσβασης. Τα κρυπτογραφημένα δεδομένα του χρήστη διαγράφηκαν.", "resetPasswordFailed": "Η επαναφορά του κωδικού πρόσβασης απέτυχε", "resetPasswordConfirmWipe": "Ο χρήστης {{username}} δεν έχει συνδεθεί από την αναβάθμιση κρυπτογράφησης, επομένως τα δεδομένα του δεν μπορούν να ανακτηθούν. Η επαναφορά τώρα θα διαγράψει τους κεντρικούς υπολογιστές, τα διαπιστευτήρια και τα αποσπάσματα κώδικα. Θέλετε να συνεχίσετε;", - "totpSectionTitle": "Έλεγχος ταυτότητας δύο παραγόντων", - "totpStatusEnabled": "Το TOTP είναι ενεργοποιημένο για αυτόν τον χρήστη", - "totpStatusDisabled": "Το TOTP δεν είναι ενεργοποιημένο για αυτόν τον χρήστη", - "disableTotp": "Καθιστώ ανίκανο", - "disableTotpConfirm": "Απενεργοποίηση ελέγχου ταυτότητας δύο παραγόντων για {{username}}; Θα μπορούν να συνδεθούν μόνο με τον κωδικό πρόσβασής τους.", - "totpDisabledSuccess": "Το TOTP απενεργοποιήθηκε", - "totpDisableFailed": "Αποτυχία απενεργοποίησης του TOTP", "manageApiKeys": "Κλειδιά API", "apiKeyNamePlaceholder": "Όνομα κλειδιού", "apiKeyCopyNotice": "Αντιγράψτε αυτό το κλειδί τώρα, δεν θα εμφανιστεί ξανά.", @@ -2576,23 +2564,6 @@ "settingsUpdates": "Ενημερώσεις", "disableUpdateChecks": "Απενεργοποίηση ελέγχων ενημερώσεων", "disableUpdateChecksDesc": "Διακοπή ελέγχου για ενημερώσεις", - "totpAuthenticator": "Επαληθευτής TOTP", - "totpEnabled": "Το 2FA είναι ενεργοποιημένο", - "totpDisabled": "Προσθέστε επιπλέον ασφάλεια σύνδεσης", - "totpAddAuthenticator": "Προσθήκη συσκευής", - "totpAddTitle": "Προσθήκη συσκευής ελέγχου ταυτότητας", - "totpAddDescription": "Επιβεβαιώστε την ταυτότητά σας για να εγγράψετε έναν άλλον ελεγκτή ταυτότητας με το ίδιο ασφαλές κλειδί TOTP.", - "totpAddInputRequired": "Εισαγάγετε τον κωδικό TOTP ή τον κωδικό πρόσβασής σας", - "totpAddFailed": "Αποτυχία προσθήκης ελέγχου ταυτότητας", - "totpAddScanInstructions": "Σαρώστε αυτόν τον κωδικό QR με τον πρόσθετο έλεγχο ταυτότητας. Οι υπάρχοντες έλεγχοι ταυτότητας θα συνεχίσουν να λειτουργούν.", - "totpAddSuccess": "Εγγράφηκε επιπλέον υπεύθυνος ελέγχου ταυτότητας", - "disable": "Καθιστώ ανίκανο", - "enable": "Καθιστώ ικανό", - "setupTotp": "Ρύθμιση TOTP", - "qrCode": "Κωδικός QR", - "totpInstructions": "Σαρώστε τον κωδικό QR ή εισαγάγετε τον μυστικό κωδικό στην εφαρμογή ελέγχου ταυτότητας και, στη συνέχεια, εισαγάγετε τον 6ψήφιο κωδικό", - "totpCodePlaceholder": "000000", - "verify": "Επαληθεύω", "changePassword": "Αλλαγή κωδικού πρόσβασης", "currentPasswordLabel": "Τρέχων κωδικός πρόσβασης", "currentPasswordPlaceholder": "Τρέχων κωδικός πρόσβασης", @@ -2638,34 +2609,7 @@ "roleUser": "Μεταχειριζόμενος", "authMethodDual": "Διπλή Εξουσιοδότηση", "authMethodOidc": "OIDC", - "totpSetupFailed": "Αποτυχία έναρξης ρύθμισης TOTP", - "totpEnter6Digits": "Εισαγάγετε έναν 6ψήφιο κωδικό", - "totpEnabledSuccess": "Ενεργοποιήθηκε ο έλεγχος ταυτότητας δύο παραγόντων", - "totpInvalidCode": "Μη έγκυρος κωδικός, δοκιμάστε ξανά", - "totpDisableInputRequired": "Εισαγάγετε τον κωδικό TOTP ή τον κωδικό πρόσβασής σας", - "totpDisabledSuccess": "Ο έλεγχος ταυτότητας δύο παραγόντων απενεργοποιήθηκε", - "totpDisableFailed": "Αποτυχία απενεργοποίησης 2FA", - "totpDisableTitle": "Απενεργοποίηση 2FA", - "totpDisablePlaceholder": "Εισαγάγετε τον κωδικό ή τον κωδικό πρόσβασης TOTP", - "totpDisableConfirm": "Απενεργοποίηση 2FA", - "totpContinueVerify": "Συνέχεια για επαλήθευση", - "totpVerifyTitle": "Επαλήθευση Κωδικού", - "totpBackupTitle": "Κωδικοί αντιγράφων ασφαλείας", - "totpDownloadBackup": "Λήψη αντιγράφων ασφαλείας κωδικών", - "passkeys": "Κλειδιά πρόσβασης", - "passkeysDesc": "Χρησιμοποιήστε διαπιστευτήρια WebAuthn/FIDO2 για να συνδεθείτε χωρίς κωδικό πρόσβασης", - "passkeyName": "Όνομα κλειδιού πρόσβασης", - "passkeyUvPreferred": "Προνομιούχος", - "passkeyUvRequired": "Υποχρεούμαι", - "passkeyUvDiscouraged": "Αποθαρρυμένος", - "addPasskey": "Προσθήκη κλειδιού πρόσβασης", - "noPasskeys": "Δεν έχουν καταχωρηθεί κλειδιά πρόσβασης.", - "passkeyAdded": "Προστέθηκε κλειδί πρόσβασης", - "passkeyAddFailed": "Αποτυχία προσθήκης κλειδιού πρόσβασης", - "passkeyDeleted": "Ο κωδικός πρόσβασης διαγράφηκε", - "passkeyDeleteFailed": "Η διαγραφή του κλειδιού πρόσβασης απέτυχε", "done": "Γινώμενος", - "secretCopied": "Το μυστικό αντιγράφηκε στο πρόχειρο", "apiKeyNameRequired": "Απαιτείται όνομα κλειδιού", "apiKeyCreated": "Κλειδί API \"{{name}}\" δημιουργήθηκε", "apiKeyCreateFailed": "Αποτυχία δημιουργίας κλειδιού API", diff --git a/src/ui/locales/translated/es_ES.json b/src/ui/locales/translated/es_ES.json index 1f9403fea..286645cce 100644 --- a/src/ui/locales/translated/es_ES.json +++ b/src/ui/locales/translated/es_ES.json @@ -1592,8 +1592,6 @@ "hasAccount": "¿Ya tiene cuenta?", "twoFactorAuth": "Autenticación de dos factores", "enterCode": "Introduzca el código de verificación", - "backupCode": "O use un código de respaldo", - "verifyCode": "Verificar código", "redirectingToApp": "Redirigiendo a la aplicación...", "sshAuthenticationRequired": "Autenticación SSH requerida", "sshNoKeyboardInteractive": "Autenticación interactiva por teclado no disponible", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "Autenticación desactivada", "authenticationDisabledDesc": "Todos los métodos de autenticación están actualmente desactivados. Póngase en contacto con su administrador.", "passwordLoginDisabledDesc": "El inicio de sesión con contraseña está desactivado. Utilice una clave de acceso o un proveedor de autenticación externo.", - "signInWithPasskey": "Iniciar sesión con clave de acceso", - "passkeyLoginFailed": "Error al iniciar sesión con clave de acceso", "attemptsRemaining": "{{count}} intentos restantes", "confirmResetDataWipe": "Esta cuenta no ha iniciado sesión desde la actualización de cifrado, por lo que sus datos almacenados no se pueden recuperar sin la contraseña antigua. Al restablecer se eliminarán permanentemente sus hosts, credenciales y fragmentos. ¿Continuar?" }, @@ -2065,7 +2061,6 @@ "saving": "Guardando...", "updateRegistrationFailed": "Error al actualizar la configuración de registro", "updatePasswordLoginFailed": "Error al actualizar la configuración de inicio de sesión por contraseña", - "cannotDisablePasswordLoginWithTotp": "No se puede deshabilitar el inicio de sesión por contraseña mientras la 2FA esté activada para uno o más usuarios. Desactive primero la 2FA.", "updateOidcAutoProvisionFailed": "Error al actualizar la configuración de aprovisionamiento automático OIDC", "updateOidcSilentLoginDefaultFailed": "Error al actualizar la configuración de inicio de sesión OIDC silencioso", "updatePasswordResetFailed": "Error al actualizar la configuración de restablecimiento de contraseña", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "Contraseña restablecida. Los datos cifrados del usuario fueron eliminados.", "resetPasswordFailed": "Error al restablecer la contraseña", "resetPasswordConfirmWipe": "{{username}} no ha iniciado sesión desde la actualización del cifrado, por lo que sus datos no se pueden recuperar. Restablecer ahora eliminará sus hosts, credenciales y fragmentos. ¿Continuar?", - "totpSectionTitle": "Autenticación de dos factores", - "totpStatusEnabled": "TOTP está habilitado para este usuario", - "totpStatusDisabled": "TOTP no está habilitado para este usuario", - "disableTotp": "Deshabilitar", - "disableTotpConfirm": "¿Deshabilitar la autenticación de dos factores para {{username}}? Podrá iniciar sesión solo con su contraseña.", - "totpDisabledSuccess": "TOTP deshabilitado", - "totpDisableFailed": "Error al deshabilitar TOTP", "manageApiKeys": "Claves API", "apiKeyNamePlaceholder": "Nombre de la clave", "apiKeyCopyNotice": "Copie esta clave ahora, no se mostrará de nuevo.", @@ -2576,23 +2564,6 @@ "settingsUpdates": "Actualizaciones", "disableUpdateChecks": "Desactivar comprobación de actualizaciones", "disableUpdateChecksDesc": "Dejar de buscar actualizaciones", - "totpAuthenticator": "Autenticador TOTP", - "totpEnabled": "2FA activada", - "totpDisabled": "Añadir seguridad adicional al inicio de sesión", - "totpAddAuthenticator": "Agregar dispositivo", - "totpAddTitle": "Agregar dispositivo de autenticación", - "totpAddDescription": "Confirma tu identidad para registrar a otro usuario con la misma clave TOTP segura.", - "totpAddInputRequired": "Ingrese su código TOTP o contraseña.", - "totpAddFailed": "No se pudo agregar el autenticador.", - "totpAddScanInstructions": "Escanee este código QR con el autenticador adicional. Los autenticadores existentes seguirán funcionando.", - "totpAddSuccess": "Autenticador adicional registrado", - "disable": "Desactivar", - "enable": "Activar", - "setupTotp": "Configurar TOTP", - "qrCode": "Código QR", - "totpInstructions": "Escanea el código QR o introduce el secreto en tu aplicación de autenticación y luego introduce el código de 6 dígitos", - "totpCodePlaceholder": "000000", - "verify": "Verificar", "changePassword": "Cambiar contraseña", "currentPasswordLabel": "Contraseña actual", "currentPasswordPlaceholder": "Contraseña actual", @@ -2638,34 +2609,7 @@ "roleUser": "Usuario", "authMethodDual": "Autenticación dual", "authMethodOidc": "OIDC", - "totpSetupFailed": "No se pudo iniciar la configuración de TOTP", - "totpEnter6Digits": "Introduce un código de 6 dígitos", - "totpEnabledSuccess": "Autenticación de dos factores activada", - "totpInvalidCode": "Código no válido, inténtalo de nuevo", - "totpDisableInputRequired": "Introduce tu código TOTP o contraseña", - "totpDisabledSuccess": "Autenticación de dos factores desactivada", - "totpDisableFailed": "No se pudo desactivar 2FA", - "totpDisableTitle": "Desactivar 2FA", - "totpDisablePlaceholder": "Introduce código TOTP o contraseña", - "totpDisableConfirm": "Desactivar 2FA", - "totpContinueVerify": "Continuar para verificar", - "totpVerifyTitle": "Verificar código", - "totpBackupTitle": "Códigos de respaldo", - "totpDownloadBackup": "Descargar códigos de respaldo", - "passkeys": "Passkeys", - "passkeysDesc": "Usa credenciales WebAuthn/FIDO2 para iniciar sesión sin contraseña", - "passkeyName": "Nombre de la passkey", - "passkeyUvPreferred": "Preferida", - "passkeyUvRequired": "Requerida", - "passkeyUvDiscouraged": "Desaconsejada", - "addPasskey": "Añadir passkey", - "noPasskeys": "No hay passkeys registradas.", - "passkeyAdded": "Passkey añadida", - "passkeyAddFailed": "No se pudo añadir la passkey", - "passkeyDeleted": "Passkey eliminada", - "passkeyDeleteFailed": "Error al eliminar la clave de acceso", "done": "Listo", - "secretCopied": "Secreto copiado al portapapeles", "apiKeyNameRequired": "El nombre de la clave es obligatorio", "apiKeyCreated": "Clave API \"{{name}}\" creada", "apiKeyCreateFailed": "Error al crear la clave API", diff --git a/src/ui/locales/translated/fi_FI.json b/src/ui/locales/translated/fi_FI.json index fb0d41e98..471d02ed6 100644 --- a/src/ui/locales/translated/fi_FI.json +++ b/src/ui/locales/translated/fi_FI.json @@ -1592,8 +1592,6 @@ "hasAccount": "Onko sinulla jo tili?", "twoFactorAuth": "Kaksivaiheinen todennus", "enterCode": "Syötä vahvistuskoodi", - "backupCode": "Tai käytä varakoodia", - "verifyCode": "Vahvista koodi", "redirectingToApp": "Ohjataan sovellukseen...", "sshAuthenticationRequired": "SSH-todennus vaaditaan", "sshNoKeyboardInteractive": "Näppäimistöinteraktiivinen todennus ei ole käytettävissä", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "Todennus poistettu käytöstä", "authenticationDisabledDesc": "Kaikki todennusmenetelmät ovat tällä hetkellä poissa käytöstä. Ota yhteyttä järjestelmänvalvojaan.", "passwordLoginDisabledDesc": "Salasanalla kirjautuminen on poistettu käytöstä. Käytä salasanaa tai ulkoista todennuspalveluntarjoajaa.", - "signInWithPasskey": "Kirjaudu sisään salasanalla", - "passkeyLoginFailed": "Salasanan kirjautuminen epäonnistui", "attemptsRemaining": "{{count}} yritystä jäljellä", "confirmResetDataWipe": "Tälle tilille ei ole kirjautunut sisään salauspäivityksen jälkeen, joten sen tallennettuja tietoja ei voida palauttaa ilman vanhaa salasanaa. Nollaaminen poistaa pysyvästi sen isännät, tunnistetiedot ja koodinpätkät. Jatketaanko?" }, @@ -2065,7 +2061,6 @@ "saving": "Tallennetaan...", "updateRegistrationFailed": "Rekisteröintiasetuksen päivittäminen epäonnistui", "updatePasswordLoginFailed": "Salasanan kirjautumisasetusten päivittäminen epäonnistui", - "cannotDisablePasswordLoginWithTotp": "Salasanalla kirjautumista ei voi poistaa käytöstä, kun 2FA on käytössä yhdelle tai useammalle käyttäjälle. Poista 2FA ensin käytöstä.", "updateOidcAutoProvisionFailed": "OIDC:n automaattisen valmistelun asetuksen päivittäminen epäonnistui.", "updateOidcSilentLoginDefaultFailed": "Hiljaisen OIDC-kirjautumisasetuksen päivittäminen epäonnistui", "updatePasswordResetFailed": "Salasanan palautusasetuksen päivittäminen epäonnistui", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "Salasanan palautus. Käyttäjän salatut tiedot on pyyhitty.", "resetPasswordFailed": "Salasanan nollaaminen epäonnistui", "resetPasswordConfirmWipe": "{{username}} ei ole kirjautunut sisään salauspäivityksen jälkeen, joten hänen tietojaan ei voida palauttaa. Nollaaminen nyt poistaa hänen palvelimensa, tunnistetietonsa ja koodinpätkänsä. Jatketaanko?", - "totpSectionTitle": "Kaksivaiheinen todennus", - "totpStatusEnabled": "TOTP on käytössä tälle käyttäjälle", - "totpStatusDisabled": "TOTP ei ole käytössä tälle käyttäjälle", - "disableTotp": "Poista käytöstä", - "disableTotpConfirm": "Poista kaksivaiheinen todennus käytöstä käyttäjälle {{username}}? He voivat kirjautua sisään pelkällä salasanallaan.", - "totpDisabledSuccess": "TOTP poistettu käytöstä", - "totpDisableFailed": "TOTP:n poistaminen käytöstä epäonnistui", "manageApiKeys": "API-avaimet", "apiKeyNamePlaceholder": "Avaimen nimi", "apiKeyCopyNotice": "Kopioi tämä avain nyt, sitä ei näytetä enää uudelleen.", @@ -2576,23 +2564,6 @@ "settingsUpdates": "Päivitykset", "disableUpdateChecks": "Poista päivitystarkistukset käytöstä", "disableUpdateChecksDesc": "Lopeta päivitysten tarkistaminen", - "totpAuthenticator": "TOTP-todentaja", - "totpEnabled": "2FA on käytössä", - "totpDisabled": "Lisää kirjautumisen turvallisuutta", - "totpAddAuthenticator": "Lisää laite", - "totpAddTitle": "Lisää todennuslaite", - "totpAddDescription": "Vahvista henkilöllisyytesi rekisteröidäksesi toisen todentajan samalla suojatulla TOTP-avaimella.", - "totpAddInputRequired": "Syötä TOTP-koodisi tai salasanasi", - "totpAddFailed": "Todentajan lisääminen epäonnistui", - "totpAddScanInstructions": "Skannaa tämä QR-koodi lisätodentajalla. Nykyiset todentajat toimivat edelleen.", - "totpAddSuccess": "Lisätodentaja rekisteröity", - "disable": "Poista käytöstä", - "enable": "Ota käyttöön", - "setupTotp": "TOTP:n määrittäminen", - "qrCode": "QR-koodi", - "totpInstructions": "Skannaa QR-koodi tai syötä salaisuus todennussovellukseesi ja syötä sitten 6-numeroinen koodi", - "totpCodePlaceholder": "000000", - "verify": "Vahvista", "changePassword": "Vaihda salasana", "currentPasswordLabel": "Nykyinen salasana", "currentPasswordPlaceholder": "Nykyinen salasana", @@ -2638,34 +2609,7 @@ "roleUser": "Käyttäjä", "authMethodDual": "Kaksoistutkimus", "authMethodOidc": "OIDC", - "totpSetupFailed": "TOTP-asennuksen aloittaminen epäonnistui", - "totpEnter6Digits": "Syötä 6-numeroinen koodi", - "totpEnabledSuccess": "Kaksivaiheinen todennus käytössä", - "totpInvalidCode": "Virheellinen koodi, yritä uudelleen", - "totpDisableInputRequired": "Syötä TOTP-koodisi tai salasanasi", - "totpDisabledSuccess": "Kaksivaiheinen todennus poistettu käytöstä", - "totpDisableFailed": "2FA:n poistaminen käytöstä epäonnistui", - "totpDisableTitle": "Poista 2FA käytöstä", - "totpDisablePlaceholder": "Syötä TOTP-koodi tai salasana", - "totpDisableConfirm": "Poista 2FA käytöstä", - "totpContinueVerify": "Jatka vahvistamiseen", - "totpVerifyTitle": "Vahvista koodi", - "totpBackupTitle": "Varakoodit", - "totpDownloadBackup": "Lataa varakoodit", - "passkeys": "Salasanat", - "passkeysDesc": "Kirjaudu sisään ilman salasanaa WebAuthn/FIDO2-tunnuksilla", - "passkeyName": "Salasanan nimi", - "passkeyUvPreferred": "Ensisijainen", - "passkeyUvRequired": "Pakollinen", - "passkeyUvDiscouraged": "Lannistunut", - "addPasskey": "Lisää salasana", - "noPasskeys": "Ei rekisteröityjä salasanoja.", - "passkeyAdded": "Salasana lisätty", - "passkeyAddFailed": "Salasanan lisääminen epäonnistui", - "passkeyDeleted": "Salasana poistettu", - "passkeyDeleteFailed": "Salasanan poistaminen epäonnistui", "done": "Tehty", - "secretCopied": "Salaisuus kopioitu leikepöydälle", "apiKeyNameRequired": "Avaimen nimi on pakollinen", "apiKeyCreated": "API-avain \"{{name}}\" luotu", "apiKeyCreateFailed": "API-avaimen luominen epäonnistui", diff --git a/src/ui/locales/translated/fr_FR.json b/src/ui/locales/translated/fr_FR.json index df3ed4dba..7d52824e3 100644 --- a/src/ui/locales/translated/fr_FR.json +++ b/src/ui/locales/translated/fr_FR.json @@ -1592,8 +1592,6 @@ "hasAccount": "Déjà un compte ?", "twoFactorAuth": "Authentification à deux facteurs", "enterCode": "Saisir le code", - "backupCode": "Ou utiliser un code de secours", - "verifyCode": "Vérifier le code", "redirectingToApp": "Redirection vers l'application...", "sshAuthenticationRequired": "Authentification SSH requise", "sshNoKeyboardInteractive": "Authentification interactive au clavier indisponible", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "Authentification désactivée", "authenticationDisabledDesc": "Toutes les méthodes d'authentification sont actuellement désactivées. Veuillez contacter votre administrateur.", "passwordLoginDisabledDesc": "La connexion par mot de passe est désactivée. Utilisez une clé d'accès ou un fournisseur d'authentification externe.", - "signInWithPasskey": "Se connecter avec une clé d'accès", - "passkeyLoginFailed": "Échec de la connexion par clé d'accès", "attemptsRemaining": "{{count}} tentatives restantes", "confirmResetDataWipe": "Ce compte ne s'est pas connecté depuis la mise à niveau du chiffrement, donc ses données stockées ne peuvent pas être récupérées sans l'ancien mot de passe. La réinitialisation supprimera définitivement ses hôtes, identifiants et extraits. Continuer ?" }, @@ -2065,7 +2061,6 @@ "saving": "Enregistrement…", "updateRegistrationFailed": "Échec de la mise à jour du paramètre d'enregistrement", "updatePasswordLoginFailed": "Échec de la mise à jour du paramètre de connexion par mot de passe", - "cannotDisablePasswordLoginWithTotp": "Impossible de désactiver la connexion par mot de passe tant que la 2FA est activée pour un ou plusieurs utilisateurs. Désactivez d'abord la 2FA.", "updateOidcAutoProvisionFailed": "Échec de la mise à jour du paramètre d'approvisionnement automatique OIDC", "updateOidcSilentLoginDefaultFailed": "Échec de la mise à jour du paramètre de connexion silencieuse OIDC", "updatePasswordResetFailed": "Échec de la mise à jour du paramètre de réinitialisation du mot de passe", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "Mot de passe réinitialisé. Les données chiffrées de l'utilisateur ont été effacées.", "resetPasswordFailed": "Échec de la réinitialisation du mot de passe", "resetPasswordConfirmWipe": "{{username}} ne s'est pas connecté depuis la mise à niveau du chiffrement, ses données ne peuvent donc pas être récupérées. La réinitialisation supprimera ses hôtes, identifiants et extraits. Continuer ?", - "totpSectionTitle": "Authentification à deux facteurs", - "totpStatusEnabled": "TOTP est activé pour cet utilisateur", - "totpStatusDisabled": "TOTP n'est pas activé pour cet utilisateur", - "disableTotp": "Désactiver", - "disableTotpConfirm": "Désactiver l'authentification à deux facteurs pour {{username}} ? Il pourra se connecter uniquement avec son mot de passe.", - "totpDisabledSuccess": "TOTP désactivé", - "totpDisableFailed": "Échec de la désactivation de TOTP", "manageApiKeys": "Clés API", "apiKeyNamePlaceholder": "Nom de la clé", "apiKeyCopyNotice": "Copiez cette clé maintenant, elle ne sera plus affichée.", @@ -2576,23 +2564,6 @@ "settingsUpdates": "Mises à jour", "disableUpdateChecks": "Désactiver la vérification des mises à jour", "disableUpdateChecksDesc": "Ne plus vérifier les mises à jour", - "totpAuthenticator": "Authentificateur TOTP", - "totpEnabled": "2FA activée", - "totpDisabled": "Ajouter une sécurité de connexion supplémentaire", - "totpAddAuthenticator": "Ajouter un appareil", - "totpAddTitle": "Ajouter un dispositif d'authentification", - "totpAddDescription": "Confirmez votre identité pour enregistrer un autre authentificateur avec la même clé TOTP sécurisée.", - "totpAddInputRequired": "Saisissez votre code TOTP ou votre mot de passe", - "totpAddFailed": "Échec de l'ajout de l'authentificateur", - "totpAddScanInstructions": "Scannez ce code QR avec l'authentificateur supplémentaire. Les authentificateurs existants continueront de fonctionner.", - "totpAddSuccess": "Authentificateur supplémentaire enregistré", - "disable": "Désactiver", - "enable": "Activer", - "setupTotp": "Configurer TOTP", - "qrCode": "Code QR", - "totpInstructions": "Scannez le code QR ou saisissez le secret dans votre application d'authentification, puis entrez le code à 6 chiffres", - "totpCodePlaceholder": "000000", - "verify": "Vérifier", "changePassword": "Modifier le mot de passe", "currentPasswordLabel": "Mot de passe actuel", "currentPasswordPlaceholder": "Mot de passe actuel", @@ -2638,34 +2609,7 @@ "roleUser": "Utilisateur", "authMethodDual": "Authentification double", "authMethodOidc": "OIDC", - "totpSetupFailed": "Échec du démarrage de la configuration TOTP", - "totpEnter6Digits": "Saisissez un code à 6 chiffres", - "totpEnabledSuccess": "Authentification à deux facteurs activée", - "totpInvalidCode": "Code invalide, veuillez réessayer", - "totpDisableInputRequired": "Saisissez votre code TOTP ou votre mot de passe", - "totpDisabledSuccess": "Authentification à deux facteurs désactivée", - "totpDisableFailed": "Échec de la désactivation de la 2FA", - "totpDisableTitle": "Désactiver l’A2F", - "totpDisablePlaceholder": "Code TOTP ou mot de passe", - "totpDisableConfirm": "Désactiver l’A2F", - "totpContinueVerify": "Poursuivre la vérification", - "totpVerifyTitle": "Vérifier le code", - "totpBackupTitle": "Codes de sauvegarde", - "totpDownloadBackup": "Télécharger les codes de sauvegarde", - "passkeys": "Clés d’accès", - "passkeysDesc": "Utilisez des identifiants WebAuthn/FIDO2 pour vous connecter sans mot de passe", - "passkeyName": "Nom de la clé d’accès", - "passkeyUvPreferred": "Préféré", - "passkeyUvRequired": "Requis", - "passkeyUvDiscouraged": "Déconseillé", - "addPasskey": "Ajouter une clé d’accès", - "noPasskeys": "Aucune clé d’accès enregistrée.", - "passkeyAdded": "Clé d’accès ajoutée", - "passkeyAddFailed": "Échec de l’ajout de la clé d’accès", - "passkeyDeleted": "Clé d’accès supprimée", - "passkeyDeleteFailed": "Échec de la suppression de la clé d'accès", "done": "Terminé", - "secretCopied": "Secret copié dans le presse-papiers", "apiKeyNameRequired": "Le nom de la clé est requis", "apiKeyCreated": "Clé API \"{{name}}\" créée", "apiKeyCreateFailed": "Échec de la création de la clé API", diff --git a/src/ui/locales/translated/he_IL.json b/src/ui/locales/translated/he_IL.json index f35120703..e490d53fe 100644 --- a/src/ui/locales/translated/he_IL.json +++ b/src/ui/locales/translated/he_IL.json @@ -1592,8 +1592,6 @@ "hasAccount": "כבר יש לך חשבון?", "twoFactorAuth": "אימות דו-שלבי", "enterCode": "הזן קוד אימות", - "backupCode": "או להשתמש בקוד גיבוי", - "verifyCode": "אימות קוד", "redirectingToApp": "מפנה לאפליקציה...", "sshAuthenticationRequired": "נדרש אימות SSH", "sshNoKeyboardInteractive": "אימות אינטראקטיבי באמצעות מקלדת אינו זמין", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "אימות מושבת", "authenticationDisabledDesc": "כל שיטות האימות מושבתות כעת. אנא צור קשר עם מנהל המערכת שלך.", "passwordLoginDisabledDesc": "כניסה באמצעות סיסמה מושבתת. השתמש בסיסמה או בספק אימות חיצוני.", - "signInWithPasskey": "כניסה עם סיסמה", - "passkeyLoginFailed": "כניסה עם סיסמה נכשלה", "attemptsRemaining": "{{count}} ניסיונות נותרו", "confirmResetDataWipe": "חשבון זה לא התחבר מאז שדרוג ההצפנה, כך שלא ניתן לשחזר את הנתונים המאוחסנים בו ללא הסיסמה הישנה. איפוס ימחק לצמיתות את המארחים, האישורים והקטעים שלו. האם להמשיך?" }, @@ -2065,7 +2061,6 @@ "saving": "חִסָכוֹן...", "updateRegistrationFailed": "עדכון הגדרות הרישום נכשל", "updatePasswordLoginFailed": "נכשל עדכון הגדרות הכניסה באמצעות סיסמה", - "cannotDisablePasswordLoginWithTotp": "לא ניתן להשבית כניסה באמצעות סיסמה כאשר 2FA מופעל עבור משתמש אחד או יותר. תחילה יש להשבית את 2FA.", "updateOidcAutoProvisionFailed": "נכשל עדכון הגדרת הקצאת הנתונים האוטומטית של OIDC", "updateOidcSilentLoginDefaultFailed": "נכשל עדכון הגדרת הכניסה השקטה של OIDC", "updatePasswordResetFailed": "נכשל עדכון הגדרת איפוס הסיסמה", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "איפוס סיסמה. הנתונים המוצפנים של המשתמש נמחקו.", "resetPasswordFailed": "איפוס הסיסמה נכשל", "resetPasswordConfirmWipe": "{{username}} לא התחבר מאז שדרוג ההצפנה, כך שלא ניתן לשחזר את הנתונים שלו. איפוס כעת ימחק את המארחים, האישורים והקטעים שלו. להמשיך?", - "totpSectionTitle": "אימות דו-שלבי", - "totpStatusEnabled": "TOTP מופעל עבור משתמש זה", - "totpStatusDisabled": "TOTP אינו מופעל עבור משתמש זה", - "disableTotp": "השבת", - "disableTotpConfirm": "להשבית אימות דו-שלבי עבור {{username}}? הם יוכלו להתחבר רק עם הסיסמה שלהם.", - "totpDisabledSuccess": "TOTP מושבת", - "totpDisableFailed": "נכשל בהשבתת TOTP", "manageApiKeys": "מפתחות API", "apiKeyNamePlaceholder": "שם המפתח", "apiKeyCopyNotice": "העתק את המפתח הזה עכשיו, הוא לא יוצג שוב.", @@ -2576,23 +2564,6 @@ "settingsUpdates": "עדכונים", "disableUpdateChecks": "השבת בדיקות עדכונים", "disableUpdateChecksDesc": "תפסיקו לבדוק עדכונים", - "totpAuthenticator": "מאמת TOTP", - "totpEnabled": "2FA מופעל", - "totpDisabled": "הוסף אבטחת התחברות נוספת", - "totpAddAuthenticator": "הוסף מכשיר", - "totpAddTitle": "הוסף מכשיר אימות", - "totpAddDescription": "אשר את זהותך כדי לרשום מאמת אחר עם אותו מפתח TOTP מאובטח.", - "totpAddInputRequired": "הזן את קוד ה-TOTP או הסיסמה שלך", - "totpAddFailed": "נכשלה הוספת המאמת", - "totpAddScanInstructions": "סרוק את קוד ה-QR הזה עם המאמת הנוסף. המאמתים הקיימים ימשיכו לפעול.", - "totpAddSuccess": "מאמת נוסף נרשם", - "disable": "השבת", - "enable": "לְאַפשֵׁר", - "setupTotp": "הגדר TOTP", - "qrCode": "קוד QR", - "totpInstructions": "סרוק את קוד ה-QR או הזן את הקוד הסודי באפליקציית האימות שלך, לאחר מכן הזן את הקוד בן 6 הספרות", - "totpCodePlaceholder": "000000", - "verify": "לְאַמֵת", "changePassword": "שינוי סיסמה", "currentPasswordLabel": "סיסמה נוכחית", "currentPasswordPlaceholder": "סיסמה נוכחית", @@ -2638,34 +2609,7 @@ "roleUser": "מִשׁתַמֵשׁ", "authMethodDual": "אימות כפול", "authMethodOidc": "OIDC", - "totpSetupFailed": "נכשל בהתחלת הגדרת TOTP", - "totpEnter6Digits": "הזן קוד בן 6 ספרות", - "totpEnabledSuccess": "אימות דו-שלבי מופעל", - "totpInvalidCode": "קוד לא תקין, אנא נסה שוב", - "totpDisableInputRequired": "הזן את קוד ה-TOTP או הסיסמה שלך", - "totpDisabledSuccess": "אימות דו-שלבי מושבת", - "totpDisableFailed": "נכשל בהשבתת 2FA", - "totpDisableTitle": "השבת 2FA", - "totpDisablePlaceholder": "הזן קוד או סיסמה של TOTP", - "totpDisableConfirm": "השבת 2FA", - "totpContinueVerify": "המשך לאימות", - "totpVerifyTitle": "אימות קוד", - "totpBackupTitle": "קודי גיבוי", - "totpDownloadBackup": "הורד קודי גיבוי", - "passkeys": "מפתחות סיסמה", - "passkeysDesc": "השתמש בפרטי הכניסה של WebAuthn/FIDO2 ללא סיסמה", - "passkeyName": "שם המפתח", - "passkeyUvPreferred": "מועדף", - "passkeyUvRequired": "דָרוּשׁ", - "passkeyUvDiscouraged": "מיואש", - "addPasskey": "הוסף סיסמה", - "noPasskeys": "אין סיסמות רשומות.", - "passkeyAdded": "סיסמה נוספת", - "passkeyAddFailed": "נכשלה הוספת סיסמה", - "passkeyDeleted": "סיסמת הגישה נמחקה", - "passkeyDeleteFailed": "מחיקת המפתח נכשלה", "done": "נַעֲשָׂה", - "secretCopied": "סוד הועתק ללוח", "apiKeyNameRequired": "נדרש שם מפתח", "apiKeyCreated": "מפתח ה-API \"{{name}}\" נוצר", "apiKeyCreateFailed": "נכשלה יצירת מפתח ה-API", diff --git a/src/ui/locales/translated/hi_IN.json b/src/ui/locales/translated/hi_IN.json index fe55406fe..35cfd9e80 100644 --- a/src/ui/locales/translated/hi_IN.json +++ b/src/ui/locales/translated/hi_IN.json @@ -1592,8 +1592,6 @@ "hasAccount": "पहले से खाता है?", "twoFactorAuth": "द्वि-कारक प्रमाणीकरण", "enterCode": "सत्यापन कोड दर्ज करें", - "backupCode": "या बैकअप कोड का उपयोग करें", - "verifyCode": "कोड सत्यापित करें", "redirectingToApp": "ऐप पर रीडायरेक्ट किया जा रहा है...", "sshAuthenticationRequired": "SSH प्रमाणीकरण आवश्यक", "sshNoKeyboardInteractive": "कीबोर्ड इंटरैक्टिव प्रमाणीकरण अनुपलब्ध", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "प्रमाणीकरण अक्षम", "authenticationDisabledDesc": "सभी प्रमाणीकरण विधियां वर्तमान में अक्षम हैं। कृपया अपने व्यवस्थापक से संपर्क करें।", "passwordLoginDisabledDesc": "पासवर्ड लॉगिन अक्षम है। पासकी या बाहरी प्रमाणीकरण प्रदाता का उपयोग करें।", - "signInWithPasskey": "पासकी से साइन इन करें", - "passkeyLoginFailed": "पासकी लॉगिन विफल", "attemptsRemaining": "{{count}} प्रयास शेष", "confirmResetDataWipe": "एन्क्रिप्शन अपग्रेड के बाद से इस खाते ने लॉगिन नहीं किया है, इसलिए पुराने पासवर्ड के बिना इसका संग्रहीत डेटा पुनर्प्राप्त नहीं किया जा सकता। रीसेट करने से इसके होस्ट, क्रेडेंशियल और स्निपेट स्थायी रूप से हटा दिए जाएंगे। जारी रखें?" }, @@ -2065,7 +2061,6 @@ "saving": "सहेजा जा रहा है...", "updateRegistrationFailed": "पंजीकरण सेटिंग अपडेट करने में विफल", "updatePasswordLoginFailed": "पासवर्ड लॉगिन सेटिंग अपडेट करने में विफल", - "cannotDisablePasswordLoginWithTotp": "जब एक या अधिक उपयोगकर्ताओं के लिए 2FA सक्षम है, तब पासवर्ड लॉगिन अक्षम नहीं किया जा सकता। पहले 2FA अक्षम करें।", "updateOidcAutoProvisionFailed": "OIDC ऑटो-प्रोविज़निंग सेटिंग अपडेट करने में विफल", "updateOidcSilentLoginDefaultFailed": "साइलेंट OIDC लॉगिन सेटिंग अपडेट करने में विफल", "updatePasswordResetFailed": "पासवर्ड रीसेट सेटिंग अपडेट करने में विफल", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "पासवर्ड रीसेट हो गया। उपयोगकर्ता का एन्क्रिप्टेड डेटा मिटा दिया गया।", "resetPasswordFailed": "पासवर्ड रीसेट करने में विफल", "resetPasswordConfirmWipe": "{{username}} ने एन्क्रिप्शन अपग्रेड के बाद से लॉग इन नहीं किया है, इसलिए उनका डेटा पुनर्प्राप्त नहीं किया जा सकता। अभी रीसेट करने से उनके होस्ट, क्रेडेंशियल और स्निपेट हटा दिए जाएँगे। जारी रखें?", - "totpSectionTitle": "दो-कारक प्रमाणीकरण", - "totpStatusEnabled": "इस उपयोगकर्ता के लिए TOTP सक्षम है", - "totpStatusDisabled": "इस उपयोगकर्ता के लिए TOTP सक्षम नहीं है", - "disableTotp": "अक्षम करें", - "disableTotpConfirm": "{{username}} के लिए दो-कारक प्रमाणीकरण अक्षम करें? वे केवल अपने पासवर्ड से लॉग इन कर सकेंगे।", - "totpDisabledSuccess": "TOTP अक्षम किया गया", - "totpDisableFailed": "TOTP अक्षम करने में विफल", "manageApiKeys": "API कुंजियाँ", "apiKeyNamePlaceholder": "कुंजी का नाम", "apiKeyCopyNotice": "इस कुंजी को अभी कॉपी करें, यह दोबारा नहीं दिखाई जाएगी।", @@ -2576,23 +2564,6 @@ "settingsUpdates": "अपडेट्स", "disableUpdateChecks": "अपडेट जांच अक्षम करें", "disableUpdateChecksDesc": "अपडेट की जांच करना बंद करें", - "totpAuthenticator": "TOTP प्रमाणक", - "totpEnabled": "2FA सक्षम है", - "totpDisabled": "अतिरिक्त लॉगिन सुरक्षा जोड़ें", - "totpAddAuthenticator": "डिवाइस जोडे", - "totpAddTitle": "प्रमाणीकरण उपकरण जोड़ें", - "totpAddDescription": "उसी सुरक्षित TOTP कुंजी के साथ किसी अन्य प्रमाणीकरणकर्ता को पंजीकृत करने के लिए अपनी पहचान की पुष्टि करें।", - "totpAddInputRequired": "अपना TOTP कोड या पासवर्ड दर्ज करें", - "totpAddFailed": "प्रमाणीकरणकर्ता जोड़ने में विफल", - "totpAddScanInstructions": "इस क्यूआर कोड को अतिरिक्त प्रमाणीकरण उपकरण से स्कैन करें। मौजूदा प्रमाणीकरण उपकरण काम करते रहेंगे।", - "totpAddSuccess": "अतिरिक्त प्रमाणीकरणकर्ता नामांकित किया गया", - "disable": "अक्षम करें", - "enable": "सक्षम करें", - "setupTotp": "TOTP सेटअप करें", - "qrCode": "QR कोड", - "totpInstructions": "QR कोड स्कैन करें या अपने प्रमाणक ऐप में सीक्रेट दर्ज करें, फिर 6-अंकीय कोड दर्ज करें", - "totpCodePlaceholder": "000000", - "verify": "सत्यापित करें", "changePassword": "पासवर्ड बदलें", "currentPasswordLabel": "वर्तमान पासवर्ड", "currentPasswordPlaceholder": "वर्तमान पासवर्ड", @@ -2638,34 +2609,7 @@ "roleUser": "उपयोगकर्ता", "authMethodDual": "दोहरा प्रमाणीकरण", "authMethodOidc": "OIDC", - "totpSetupFailed": "TOTP सेटअप शुरू करने में विफल", - "totpEnter6Digits": "6-अंकीय कोड दर्ज करें", - "totpEnabledSuccess": "दो-कारक प्रमाणीकरण सक्षम किया गया", - "totpInvalidCode": "अमान्य कोड, कृपया पुनः प्रयास करें", - "totpDisableInputRequired": "अपना TOTP कोड या पासवर्ड दर्ज करें", - "totpDisabledSuccess": "दो-कारक प्रमाणीकरण अक्षम किया गया", - "totpDisableFailed": "2FA अक्षम करने में विफल", - "totpDisableTitle": "2FA अक्षम करें", - "totpDisablePlaceholder": "TOTP कोड या पासवर्ड दर्ज करें", - "totpDisableConfirm": "2FA अक्षम करें", - "totpContinueVerify": "सत्यापन जारी रखें", - "totpVerifyTitle": "कोड सत्यापित करें", - "totpBackupTitle": "बैकअप कोड", - "totpDownloadBackup": "बैकअप कोड डाउनलोड करें", - "passkeys": "पासकीज़", - "passkeysDesc": "बिना पासवर्ड साइन इन करने के लिए WebAuthn/FIDO2 क्रेडेंशियल का उपयोग करें", - "passkeyName": "पासकी नाम", - "passkeyUvPreferred": "पसंदीदा", - "passkeyUvRequired": "आवश्यक", - "passkeyUvDiscouraged": "हतोत्साहित", - "addPasskey": "पासकी जोड़ें", - "noPasskeys": "कोई पासकी पंजीकृत नहीं है।", - "passkeyAdded": "पासकी जोड़ी गई", - "passkeyAddFailed": "पासकी जोड़ने में विफल", - "passkeyDeleted": "पासकी हटाई गई", - "passkeyDeleteFailed": "पासकी हटाने में विफल", "done": "पूर्ण", - "secretCopied": "सीक्रेट क्लिपबोर्ड पर कॉपी किया गया", "apiKeyNameRequired": "कुंजी नाम आवश्यक है", "apiKeyCreated": "API कुंजी \"{{name}}\" बनाई गई", "apiKeyCreateFailed": "API कुंजी बनाने में विफल", diff --git a/src/ui/locales/translated/hu_HU.json b/src/ui/locales/translated/hu_HU.json index 65dedd963..5ff8290db 100644 --- a/src/ui/locales/translated/hu_HU.json +++ b/src/ui/locales/translated/hu_HU.json @@ -1592,8 +1592,6 @@ "hasAccount": "Már van fiókja?", "twoFactorAuth": "Kétfaktoros hitelesítés", "enterCode": "Írja be az ellenőrző kódot", - "backupCode": "Vagy használjon biztonsági kódot", - "verifyCode": "Kód ellenőrzése", "redirectingToApp": "Átirányítás az alkalmazáshoz...", "sshAuthenticationRequired": "SSH-hitelesítés szükséges", "sshNoKeyboardInteractive": "Billentyűzet-interaktív hitelesítés nem érhető el", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "Hitelesítés letiltva", "authenticationDisabledDesc": "Jelenleg minden hitelesítési módszer le van tiltva. Kérjük, vegye fel a kapcsolatot a rendszergazdával.", "passwordLoginDisabledDesc": "A jelszóval történő bejelentkezés le van tiltva. Használjon jelszót vagy külső hitelesítési szolgáltatót.", - "signInWithPasskey": "Bejelentkezés jelszóval", - "passkeyLoginFailed": "Jelszóval történő bejelentkezés sikertelen", "attemptsRemaining": "{{count}} próbálkozás van hátra", "confirmResetDataWipe": "Ez a fiók a titkosítás frissítése óta nem jelentkezett be, így a tárolt adatok nem állíthatók vissza a régi jelszó nélkül. A visszaállítás véglegesen törli a gazdagépeket, a hitelesítő adatokat és a kódrészleteket. Folytatja?" }, @@ -2065,7 +2061,6 @@ "saving": "Megtakarítás...", "updateRegistrationFailed": "Nem sikerült frissíteni a regisztrációs beállításokat", "updatePasswordLoginFailed": "Nem sikerült frissíteni a jelszó bejelentkezési beállítását", - "cannotDisablePasswordLoginWithTotp": "Nem lehet letiltani a jelszavas bejelentkezést, amíg a 2FA engedélyezve van egy vagy több felhasználó számára. Először tiltsa le a 2FA-t.", "updateOidcAutoProvisionFailed": "Nem sikerült frissíteni az OIDC automatikus kiépítési beállítását", "updateOidcSilentLoginDefaultFailed": "Nem sikerült frissíteni a csendes OIDC bejelentkezési beállítást", "updatePasswordResetFailed": "Nem sikerült frissíteni a jelszó-visszaállítási beállítást", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "Jelszó visszaállítása. A felhasználó titkosított adatait törölték.", "resetPasswordFailed": "Nem sikerült visszaállítani a jelszót", "resetPasswordConfirmWipe": "{{username}} nem jelentkezett be a titkosítás frissítése óta, így az adatai nem állíthatók vissza. A visszaállítás most törli a gazdagépeit, hitelesítő adatait és kódrészleteit. Folytatja?", - "totpSectionTitle": "Kétfaktoros hitelesítés", - "totpStatusEnabled": "A TOTP engedélyezve van ehhez a felhasználóhoz.", - "totpStatusDisabled": "A TOTP nincs engedélyezve ehhez a felhasználóhoz.", - "disableTotp": "Letiltás", - "disableTotpConfirm": "Letiltja a kétfaktoros hitelesítést a {{username}}felhasználó számára? Csak a jelszavával tudnak majd bejelentkezni.", - "totpDisabledSuccess": "TOTP letiltva", - "totpDisableFailed": "Nem sikerült letiltani a TOTP-t", "manageApiKeys": "API-kulcsok", "apiKeyNamePlaceholder": "Kulcs neve", "apiKeyCopyNotice": "Másold le ezt a kulcsot most, nem fog újra megjelenni.", @@ -2576,23 +2564,6 @@ "settingsUpdates": "Frissítések", "disableUpdateChecks": "Frissítés-ellenőrzések letiltása", "disableUpdateChecksDesc": "Frissítések keresésének leállítása", - "totpAuthenticator": "TOTP hitelesítő", - "totpEnabled": "2FA engedélyezve", - "totpDisabled": "Fokozott bejelentkezési biztonság", - "totpAddAuthenticator": "Eszköz hozzáadása", - "totpAddTitle": "Hitelesítő eszköz hozzáadása", - "totpAddDescription": "Erősítse meg személyazonosságát, hogy egy másik hitelesítőt regisztrálhasson ugyanazzal a biztonságos TOTP-kulccsal.", - "totpAddInputRequired": "Írd be a TOTP kódodat vagy jelszavadat", - "totpAddFailed": "Nem sikerült hozzáadni a hitelesítőt", - "totpAddScanInstructions": "Olvasd be ezt a QR-kódot a további hitelesítővel. A meglévő hitelesítők továbbra is működni fognak.", - "totpAddSuccess": "További hitelesítő regisztrálva", - "disable": "Letiltás", - "enable": "Engedélyezés", - "setupTotp": "TOTP beállítása", - "qrCode": "QR-kód", - "totpInstructions": "Olvasd be a QR-kódot, vagy írd be a titkos kódot a hitelesítő alkalmazásodba, majd írd be a 6 jegyű kódot", - "totpCodePlaceholder": "000000", - "verify": "Ellenőrzés", "changePassword": "Jelszó módosítása", "currentPasswordLabel": "Jelenlegi jelszó", "currentPasswordPlaceholder": "Jelenlegi jelszó", @@ -2638,34 +2609,7 @@ "roleUser": "Felhasználó", "authMethodDual": "Kettős hitelesítés", "authMethodOidc": "OIDC", - "totpSetupFailed": "Nem sikerült elindítani a TOTP beállítását", - "totpEnter6Digits": "Adjon meg egy 6 jegyű kódot", - "totpEnabledSuccess": "Kétfaktoros hitelesítés engedélyezve", - "totpInvalidCode": "Érvénytelen kód, kérjük, próbálja újra", - "totpDisableInputRequired": "Írd be a TOTP kódodat vagy jelszavadat", - "totpDisabledSuccess": "Kétfaktoros hitelesítés letiltva", - "totpDisableFailed": "Nem sikerült letiltani a 2FA-t", - "totpDisableTitle": "2FA letiltása", - "totpDisablePlaceholder": "Írja be a TOTP kódot vagy jelszót", - "totpDisableConfirm": "2FA letiltása", - "totpContinueVerify": "Tovább az ellenőrzéshez", - "totpVerifyTitle": "Kód ellenőrzése", - "totpBackupTitle": "Biztonsági kódok", - "totpDownloadBackup": "Tartalékkódok letöltése", - "passkeys": "Jelszavak", - "passkeysDesc": "Jelszó nélküli bejelentkezéshez használja a WebAuthn/FIDO2 hitelesítő adatokat", - "passkeyName": "Jelszó neve", - "passkeyUvPreferred": "Előnyben részesített", - "passkeyUvRequired": "Kívánt", - "passkeyUvDiscouraged": "Elbátortalanodva", - "addPasskey": "Jelszó hozzáadása", - "noPasskeys": "Nincsenek regisztrált jelszók.", - "passkeyAdded": "Hozzáadott jelszó", - "passkeyAddFailed": "Nem sikerült hozzáadni a jelszót", - "passkeyDeleted": "Jelszó törölve", - "passkeyDeleteFailed": "Nem sikerült törölni a jelszót", "done": "Kész", - "secretCopied": "Titkos kód a vágólapra másolva", "apiKeyNameRequired": "Kulcsnév megadása kötelező", "apiKeyCreated": "„{{name}}” API-kulcs létrehozva", "apiKeyCreateFailed": "Nem sikerült létrehozni az API-kulcsot", diff --git a/src/ui/locales/translated/id_ID.json b/src/ui/locales/translated/id_ID.json index 63de718d9..f9e6b64bd 100644 --- a/src/ui/locales/translated/id_ID.json +++ b/src/ui/locales/translated/id_ID.json @@ -1592,8 +1592,6 @@ "hasAccount": "Sudah punya akun?", "twoFactorAuth": "Autentikasi Dua Faktor", "enterCode": "Masukkan kode verifikasi", - "backupCode": "Atau gunakan kode cadangan", - "verifyCode": "Verifikasi Kode", "redirectingToApp": "Mengalihkan ke aplikasi...", "sshAuthenticationRequired": "Autentikasi SSH Diperlukan", "sshNoKeyboardInteractive": "Autentikasi Interaktif Keyboard Tidak Tersedia", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "Autentikasi Dinonaktifkan", "authenticationDisabledDesc": "Semua metode otentikasi saat ini dinonaktifkan. Silakan hubungi administrator Anda.", "passwordLoginDisabledDesc": "Login dengan kata sandi dinonaktifkan. Gunakan kata sandi atau penyedia autentikasi eksternal.", - "signInWithPasskey": "Masuk dengan kata sandi", - "passkeyLoginFailed": "Login dengan kata sandi gagal.", "attemptsRemaining": "{{count}} percobaan tersisa", "confirmResetDataWipe": "Akun ini belum masuk sejak peningkatan enkripsi, jadi data yang tersimpan tidak dapat dipulihkan tanpa kata sandi lama. Mengatur ulang akan menghapus host, kredensial, dan cuplikan secara permanen. Lanjutkan?" }, @@ -2065,7 +2061,6 @@ "saving": "Penghematan...", "updateRegistrationFailed": "Gagal memperbarui pengaturan pendaftaran", "updatePasswordLoginFailed": "Gagal memperbarui pengaturan login kata sandi.", - "cannotDisablePasswordLoginWithTotp": "Tidak dapat menonaktifkan login kata sandi saat otentikasi dua faktor (2FA) diaktifkan untuk satu atau lebih pengguna. Nonaktifkan 2FA terlebih dahulu.", "updateOidcAutoProvisionFailed": "Gagal memperbarui pengaturan penyediaan otomatis OIDC.", "updateOidcSilentLoginDefaultFailed": "Gagal memperbarui pengaturan login OIDC senyap", "updatePasswordResetFailed": "Gagal memperbarui pengaturan pengaturan ulang kata sandi.", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "Kata sandi telah direset. Data terenkripsi pengguna telah dihapus.", "resetPasswordFailed": "Gagal mengatur ulang kata sandi", "resetPasswordConfirmWipe": "{{username}} belum masuk sejak peningkatan enkripsi, jadi data mereka tidak dapat dipulihkan. Mengatur ulang sekarang akan menghapus host, kredensial, dan cuplikan mereka. Lanjutkan?", - "totpSectionTitle": "Autentikasi Dua Faktor", - "totpStatusEnabled": "TOTP diaktifkan untuk pengguna ini.", - "totpStatusDisabled": "TOTP tidak diaktifkan untuk pengguna ini.", - "disableTotp": "Cacat", - "disableTotpConfirm": "Nonaktifkan otentikasi dua faktor untuk {{username}}? Mereka akan dapat masuk hanya dengan kata sandi mereka.", - "totpDisabledSuccess": "TOTP dinonaktifkan", - "totpDisableFailed": "Gagal menonaktifkan TOTP", "manageApiKeys": "Kunci API", "apiKeyNamePlaceholder": "Nama kunci", "apiKeyCopyNotice": "Salin kunci ini sekarang, kunci ini tidak akan ditampilkan lagi.", @@ -2576,23 +2564,6 @@ "settingsUpdates": "Pembaruan", "disableUpdateChecks": "Nonaktifkan Pemeriksaan Pembaruan", "disableUpdateChecksDesc": "Berhenti memeriksa pembaruan.", - "totpAuthenticator": "Otentikator TOTP", - "totpEnabled": "Otentikasi dua faktor (2FA) diaktifkan.", - "totpDisabled": "Tambahkan keamanan login ekstra", - "totpAddAuthenticator": "Tambahkan perangkat", - "totpAddTitle": "Tambahkan perangkat otentikasi", - "totpAddDescription": "Konfirmasikan identitas Anda untuk mendaftarkan otentikator lain dengan kunci TOTP aman yang sama.", - "totpAddInputRequired": "Masukkan kode TOTP atau kata sandi Anda.", - "totpAddFailed": "Gagal menambahkan autentikator", - "totpAddScanInstructions": "Pindai kode QR ini dengan otentikator tambahan. Otentikator yang sudah ada akan tetap berfungsi.", - "totpAddSuccess": "Autentikator tambahan terdaftar", - "disable": "Cacat", - "enable": "Memungkinkan", - "setupTotp": "Pengaturan TOTP", - "qrCode": "Kode QR", - "totpInstructions": "Pindai kode QR atau masukkan kode rahasia di aplikasi otentikasi Anda, lalu masukkan kode 6 digit.", - "totpCodePlaceholder": "000000", - "verify": "Memeriksa", "changePassword": "Ubah Kata Sandi", "currentPasswordLabel": "Kata Sandi Saat Ini", "currentPasswordPlaceholder": "Kata sandi saat ini", @@ -2638,34 +2609,7 @@ "roleUser": "Pengguna", "authMethodDual": "Otorisasi Ganda", "authMethodOidc": "OIDC", - "totpSetupFailed": "Gagal memulai pengaturan TOTP", - "totpEnter6Digits": "Masukkan kode 6 digit", - "totpEnabledSuccess": "Autentikasi dua faktor diaktifkan", - "totpInvalidCode": "Kode tidak valid, silakan coba lagi.", - "totpDisableInputRequired": "Masukkan kode TOTP atau kata sandi Anda.", - "totpDisabledSuccess": "Autentikasi dua faktor dinonaktifkan", - "totpDisableFailed": "Gagal menonaktifkan 2FA.", - "totpDisableTitle": "Nonaktifkan otentikasi dua faktor (2FA).", - "totpDisablePlaceholder": "Masukkan kode TOTP atau kata sandi.", - "totpDisableConfirm": "Nonaktifkan otentikasi dua faktor (2FA).", - "totpContinueVerify": "Lanjutkan Verifikasi", - "totpVerifyTitle": "Verifikasi Kode", - "totpBackupTitle": "Kode Cadangan", - "totpDownloadBackup": "Unduh Kode Cadangan", - "passkeys": "Kode akses", - "passkeysDesc": "Gunakan kredensial WebAuthn/FIDO2 untuk masuk tanpa kata sandi.", - "passkeyName": "Nama kata sandi", - "passkeyUvPreferred": "Lebih disukai", - "passkeyUvRequired": "Diperlukan", - "passkeyUvDiscouraged": "Berkecil hati", - "addPasskey": "Tambahkan Kata Sandi", - "noPasskeys": "Tidak ada kata sandi yang terdaftar.", - "passkeyAdded": "Kata sandi telah ditambahkan", - "passkeyAddFailed": "Gagal menambahkan kata sandi.", - "passkeyDeleted": "Kata sandi telah dihapus.", - "passkeyDeleteFailed": "Gagal menghapus kata sandi.", "done": "Selesai", - "secretCopied": "Rahasia disalin ke papan klip", "apiKeyNameRequired": "Nama kunci wajib diisi.", "apiKeyCreated": "Kunci API \"{{name}}\" berhasil dibuat", "apiKeyCreateFailed": "Gagal membuat kunci API", diff --git a/src/ui/locales/translated/it_IT.json b/src/ui/locales/translated/it_IT.json index 9f704f404..17e914115 100644 --- a/src/ui/locales/translated/it_IT.json +++ b/src/ui/locales/translated/it_IT.json @@ -1592,8 +1592,6 @@ "hasAccount": "Hai già un account?", "twoFactorAuth": "Autenticazione a due fattori", "enterCode": "Inserisci codice di verifica", - "backupCode": "Oppure usa codice di backup", - "verifyCode": "Verifica codice", "redirectingToApp": "Reindirizzamento all'app in corso...", "sshAuthenticationRequired": "Autenticazione SSH richiesta", "sshNoKeyboardInteractive": "Autenticazione keyboard-interactive non disponibile", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "Autenticazione disabilitata", "authenticationDisabledDesc": "Tutti i metodi di autenticazione sono attualmente disabilitati. Contatta l'amministratore.", "passwordLoginDisabledDesc": "L'accesso con password è disabilitato. Utilizza una passkey o un provider di autenticazione esterno.", - "signInWithPasskey": "Accedi con passkey", - "passkeyLoginFailed": "Accesso con passkey non riuscito", "attemptsRemaining": "{{count}} tentativi rimanenti", "confirmResetDataWipe": "Questo account non ha effettuato l'accesso dopo l'aggiornamento della crittografia, pertanto i dati archiviati non possono essere recuperati senza la vecchia password. Il ripristino eliminerà definitivamente host, credenziali e snippet. Continuare?" }, @@ -2065,7 +2061,6 @@ "saving": "Salvataggio in corso...", "updateRegistrationFailed": "Aggiornamento impostazione registrazione non riuscito", "updatePasswordLoginFailed": "Aggiornamento impostazione accesso con password non riuscito", - "cannotDisablePasswordLoginWithTotp": "Impossibile disabilitare l'accesso con password mentre la 2FA è abilitata per uno o più utenti. Disabilita prima la 2FA.", "updateOidcAutoProvisionFailed": "Aggiornamento impostazione provisioning automatico OIDC non riuscito", "updateOidcSilentLoginDefaultFailed": "Aggiornamento impostazione accesso OIDC silenzioso non riuscito", "updatePasswordResetFailed": "Aggiornamento impostazione reimpostazione password non riuscito", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "Password reimpostata. I dati crittografati dell'utente sono stati cancellati.", "resetPasswordFailed": "Reimpostazione password non riuscita", "resetPasswordConfirmWipe": "{{username}} non ha effettuato l'accesso dall'aggiornamento della crittografia, pertanto i suoi dati non possono essere recuperati. Con la reimpostazione verranno eliminati i suoi host, credenziali e frammenti. Continuare?", - "totpSectionTitle": "Autenticazione a due fattori", - "totpStatusEnabled": "TOTP è abilitato per questo utente", - "totpStatusDisabled": "TOTP non è abilitato per questo utente", - "disableTotp": "Disabilita", - "disableTotpConfirm": "Disabilitare l’autenticazione a due fattori per {{username}}? L’utente potrà accedere solo con la password.", - "totpDisabledSuccess": "TOTP disabilitato", - "totpDisableFailed": "Disabilitazione TOTP non riuscita", "manageApiKeys": "Chiavi API", "apiKeyNamePlaceholder": "Nome chiave", "apiKeyCopyNotice": "Copia questa chiave ora, non verrà più mostrata.", @@ -2576,23 +2564,6 @@ "settingsUpdates": "Aggiornamenti", "disableUpdateChecks": "Disabilita controllo aggiornamenti", "disableUpdateChecksDesc": "Non verificare la presenza di aggiornamenti", - "totpAuthenticator": "Autenticatore TOTP", - "totpEnabled": "2FA è attivata", - "totpDisabled": "Aggiungi sicurezza aggiuntiva all'accesso", - "totpAddAuthenticator": "Aggiungi dispositivo", - "totpAddTitle": "Aggiungi dispositivo di autenticazione", - "totpAddDescription": "Conferma la tua identità per registrare un altro autenticatore con la stessa chiave TOTP sicura.", - "totpAddInputRequired": "Inserisci il tuo codice TOTP o la password", - "totpAddFailed": "Impossibile aggiungere l'autenticatore", - "totpAddScanInstructions": "Scansiona questo codice QR con l'autenticatore aggiuntivo. Gli autenticatori già in uso continueranno a funzionare.", - "totpAddSuccess": "Autenticatore aggiuntivo registrato", - "disable": "Disabilita", - "enable": "Abilita", - "setupTotp": "Configura TOTP", - "qrCode": "Codice QR", - "totpInstructions": "Scansiona il codice QR o inserisci il segreto nella tua app di autenticazione, quindi inserisci il codice a 6 cifre", - "totpCodePlaceholder": "000000", - "verify": "Verifica", "changePassword": "Modifica password", "currentPasswordLabel": "Password attuale", "currentPasswordPlaceholder": "Password attuale", @@ -2638,34 +2609,7 @@ "roleUser": "Utente", "authMethodDual": "Doppia autenticazione", "authMethodOidc": "OIDC", - "totpSetupFailed": "Impossibile avviare la configurazione TOTP", - "totpEnter6Digits": "Inserisci un codice a 6 cifre", - "totpEnabledSuccess": "Autenticazione a due fattori abilitata", - "totpInvalidCode": "Codice non valido, riprova", - "totpDisableInputRequired": "Inserisci il tuo codice TOTP o la password", - "totpDisabledSuccess": "Autenticazione a due fattori disabilitata", - "totpDisableFailed": "Impossibile disabilitare la 2FA", - "totpDisableTitle": "Disabilita 2FA", - "totpDisablePlaceholder": "Inserisci codice TOTP o password", - "totpDisableConfirm": "Disabilita 2FA", - "totpContinueVerify": "Procedi alla verifica", - "totpVerifyTitle": "Verifica codice", - "totpBackupTitle": "Codici di backup", - "totpDownloadBackup": "Scarica i codici di backup", - "passkeys": "Passkeys", - "passkeysDesc": "Utilizza credenziali WebAuthn/FIDO2 per accedere senza password", - "passkeyName": "Nome passkey", - "passkeyUvPreferred": "Preferita", - "passkeyUvRequired": "Obbligatoria", - "passkeyUvDiscouraged": "Sconsigliata", - "addPasskey": "Aggiungi passkey", - "noPasskeys": "Nessuna passkey registrata.", - "passkeyAdded": "Passkey aggiunta", - "passkeyAddFailed": "Impossibile aggiungere la passkey", - "passkeyDeleted": "Passkey eliminata", - "passkeyDeleteFailed": "Impossibile eliminare la passkey", "done": "Fatto", - "secretCopied": "Segreto copiato negli appunti", "apiKeyNameRequired": "Il nome della chiave è obbligatorio", "apiKeyCreated": "Chiave API \"{{name}}\" creata", "apiKeyCreateFailed": "Impossibile creare la chiave API", diff --git a/src/ui/locales/translated/ja_JP.json b/src/ui/locales/translated/ja_JP.json index 44721c00d..badc44a3f 100644 --- a/src/ui/locales/translated/ja_JP.json +++ b/src/ui/locales/translated/ja_JP.json @@ -1592,8 +1592,6 @@ "hasAccount": "すでにアカウントをお持ちですか?", "twoFactorAuth": "二要素認証", "enterCode": "認証コードを入力", - "backupCode": "またはバックアップコードを使用", - "verifyCode": "認証コードを確認", "redirectingToApp": "アプリにリダイレクト中...", "sshAuthenticationRequired": "SSH認証が必要です", "sshNoKeyboardInteractive": "キーボードインタラクティブ認証は利用できません", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "認証が無効です", "authenticationDisabledDesc": "現在、すべての認証方法が無効になっています。管理者に連絡してください。", "passwordLoginDisabledDesc": "パスワードログインは無効です。パスキーまたは外部認証プロバイダーを使用してください。", - "signInWithPasskey": "パスキーでサインイン", - "passkeyLoginFailed": "パスキーログインに失敗しました", "attemptsRemaining": "残り{{count}}回", "confirmResetDataWipe": "このアカウントは暗号化アップグレード以降ログインしていないため、旧パスワードがないと保存データを復旧できません。リセットすると、ホスト、認証情報、スニペットが完全に削除されます。続行しますか?" }, @@ -2065,7 +2061,6 @@ "saving": "保存中...", "updateRegistrationFailed": "登録設定の更新に失敗しました", "updatePasswordLoginFailed": "パスワードログイン設定の更新に失敗しました", - "cannotDisablePasswordLoginWithTotp": "1人以上のユーザーで2FAが有効な間はパスワードログインを無効にできません。最初に2FAを無効にしてください。", "updateOidcAutoProvisionFailed": "OIDC自動プロビジョニング設定の更新に失敗しました", "updateOidcSilentLoginDefaultFailed": "サイレントOIDCログイン設定の更新に失敗しました", "updatePasswordResetFailed": "パスワードリセット設定の更新に失敗しました", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "パスワードがリセットされました。ユーザーの暗号化データは消去されました。", "resetPasswordFailed": "パスワードのリセットに失敗しました", "resetPasswordConfirmWipe": "{{username}} は暗号化アップグレード以降にログインしていないため、データを復旧できません。今リセットするとホスト、認証情報、スニペットが削除されます。続行しますか?", - "totpSectionTitle": "二要素認証", - "totpStatusEnabled": "このユーザーはTOTPが有効です", - "totpStatusDisabled": "このユーザーはTOTPが無効です", - "disableTotp": "無効化", - "disableTotpConfirm": "{{username}} の二要素認証を無効にしますか? パスワードのみでログインできるようになります。", - "totpDisabledSuccess": "TOTPが無効化されました", - "totpDisableFailed": "TOTPの無効化に失敗しました", "manageApiKeys": "APIキー", "apiKeyNamePlaceholder": "キー名", "apiKeyCopyNotice": "このキーを今コピーしてください。再度表示されません。", @@ -2576,23 +2564,6 @@ "settingsUpdates": "アップデート", "disableUpdateChecks": "アップデートチェックを無効化", "disableUpdateChecksDesc": "アップデートの確認を停止する", - "totpAuthenticator": "TOTP認証", - "totpEnabled": "2FAが有効です", - "totpDisabled": "ログインのセキュリティを強化", - "totpAddAuthenticator": "デバイスを追加する", - "totpAddTitle": "認証デバイスを追加する", - "totpAddDescription": "本人確認を行うことで、同じ安全なTOTPキーを持つ別の認証アプリを登録できます。", - "totpAddInputRequired": "TOTPコードまたはパスワードを入力してください", - "totpAddFailed": "認証システムの追加に失敗しました", - "totpAddScanInstructions": "追加の認証デバイスでこのQRコードをスキャンしてください。既存の認証デバイスは引き続きご利用いただけます。", - "totpAddSuccess": "追加の認証デバイスを登録しました", - "disable": "無効にする", - "enable": "有効にする", - "setupTotp": "TOTPを設定", - "qrCode": "QRコード", - "totpInstructions": "QRコードをスキャンするか、認証アプリにシークレットを入力し、6桁のコードを入力してください", - "totpCodePlaceholder": "000000", - "verify": "確認", "changePassword": "パスワード変更", "currentPasswordLabel": "現在のパスワード", "currentPasswordPlaceholder": "現在のパスワード", @@ -2638,34 +2609,7 @@ "roleUser": "ユーザー", "authMethodDual": "二要素認証", "authMethodOidc": "OIDC", - "totpSetupFailed": "TOTPのセットアップを開始できませんでした", - "totpEnter6Digits": "6桁のコードを入力してください", - "totpEnabledSuccess": "二要素認証が有効になりました", - "totpInvalidCode": "コードが無効です。もう一度お試しください", - "totpDisableInputRequired": "TOTPコードまたはパスワードを入力してください", - "totpDisabledSuccess": "二要素認証が無効になりました", - "totpDisableFailed": "2FAの無効化に失敗しました", - "totpDisableTitle": "2FAを無効にする", - "totpDisablePlaceholder": "TOTPコードまたはパスワードを入力", - "totpDisableConfirm": "2FAを無効にする", - "totpContinueVerify": "確認に進む", - "totpVerifyTitle": "コードの確認", - "totpBackupTitle": "バックアップコード", - "totpDownloadBackup": "バックアップコードをダウンロード", - "passkeys": "パスキー", - "passkeysDesc": "WebAuthn/FIDO2認証情報を使用してパスワードなしでサインインできます", - "passkeyName": "パスキー名", - "passkeyUvPreferred": "優先", - "passkeyUvRequired": "必須", - "passkeyUvDiscouraged": "非推奨", - "addPasskey": "パスキーを追加", - "noPasskeys": "登録されたパスキーはありません。", - "passkeyAdded": "パスキーが追加されました", - "passkeyAddFailed": "パスキーの追加に失敗しました", - "passkeyDeleted": "パスキーが削除されました", - "passkeyDeleteFailed": "パスキーの削除に失敗しました", "done": "完了", - "secretCopied": "シークレットをクリップボードにコピーしました", "apiKeyNameRequired": "キー名は必須です", "apiKeyCreated": "APIキー「{{name}}」が作成されました", "apiKeyCreateFailed": "APIキーの作成に失敗しました", diff --git a/src/ui/locales/translated/ko_KR.json b/src/ui/locales/translated/ko_KR.json index 62ec30420..f8f360d1c 100644 --- a/src/ui/locales/translated/ko_KR.json +++ b/src/ui/locales/translated/ko_KR.json @@ -1592,8 +1592,6 @@ "hasAccount": "이미 계정이 있으신가요?", "twoFactorAuth": "2단계 인증", "enterCode": "인증 코드 입력", - "backupCode": "또는 백업 코드 사용", - "verifyCode": "코드 확인", "redirectingToApp": "앱으로 리디렉션 중...", "sshAuthenticationRequired": "SSH 인증 필요", "sshNoKeyboardInteractive": "키보드 대화식 인증 사용 불가", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "인증 비활성화됨", "authenticationDisabledDesc": "현재 모든 인증 방법이 비활성화되어 있습니다. 관리자에게 문의하세요.", "passwordLoginDisabledDesc": "비밀번호 로그인이 비활성화되었습니다. Passkey 또는 외부 인증 제공자를 사용하세요.", - "signInWithPasskey": "Passkey로 로그인", - "passkeyLoginFailed": "패스키 로그인 실패", "attemptsRemaining": "{{count}}회 남음", "confirmResetDataWipe": "이 계정은 암호화 업그레이드 이후 로그인하지 않아 이전 비밀번호 없이는 저장된 데이터를 복구할 수 없습니다. 초기화하면 호스트, 자격 증명 및 스니펫이 영구적으로 삭제됩니다. 계속하시겠습니까?" }, @@ -2065,7 +2061,6 @@ "saving": "저장 중...", "updateRegistrationFailed": "등록 설정 업데이트에 실패했습니다.", "updatePasswordLoginFailed": "비밀번호 로그인 설정 업데이트에 실패했습니다.", - "cannotDisablePasswordLoginWithTotp": "하나 이상의 사용자에게 2FA가 활성화되어 있는 동안에는 비밀번호 로그인을 비활성화할 수 없습니다. 먼저 2FA를 비활성화하세요.", "updateOidcAutoProvisionFailed": "OIDC 자동 프로비저닝 설정 업데이트에 실패했습니다.", "updateOidcSilentLoginDefaultFailed": "자동 OIDC 로그인 설정 업데이트에 실패했습니다.", "updatePasswordResetFailed": "비밀번호 재설정 설정 업데이트에 실패했습니다.", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "비밀번호가 재설정되었습니다. 사용자의 암호화된 데이터가 삭제되었습니다.", "resetPasswordFailed": "비밀번호 재설정에 실패했습니다.", "resetPasswordConfirmWipe": "{{username}}님이 암호화 업그레이드 이후 로그인하지 않아 데이터를 복구할 수 없습니다. 지금 재설정하면 호스트, 자격 증명 및 스니펫이 삭제됩니다. 계속하시겠습니까?", - "totpSectionTitle": "2단계 인증", - "totpStatusEnabled": "이 사용자에 대해 TOTP가 활성화되었습니다.", - "totpStatusDisabled": "이 사용자에 대해 TOTP가 비활성화되었습니다.", - "disableTotp": "비활성화", - "disableTotpConfirm": "{{username}}님의 2단계 인증을 비활성화하시겠습니까? 비밀번호만으로 로그인할 수 있게 됩니다.", - "totpDisabledSuccess": "TOTP가 비활성화되었습니다.", - "totpDisableFailed": "TOTP 비활성화에 실패했습니다.", "manageApiKeys": "API 키", "apiKeyNamePlaceholder": "키 이름", "apiKeyCopyNotice": "이 키를 지금 복사하세요. 다시 표시되지 않습니다.", @@ -2576,23 +2564,6 @@ "settingsUpdates": "업데이트", "disableUpdateChecks": "업데이트 확인 비활성화", "disableUpdateChecksDesc": "업데이트 확인 중지", - "totpAuthenticator": "TOTP 인증기", - "totpEnabled": "2FA가 활성화되었습니다", - "totpDisabled": "추가 로그인 보안을 설정하세요", - "totpAddAuthenticator": "기기 추가", - "totpAddTitle": "인증 장치 추가", - "totpAddDescription": "본인 인증을 완료하면 동일한 보안 TOTP 키를 사용하여 다른 인증자를 등록할 수 있습니다.", - "totpAddInputRequired": "TOTP 코드 또는 비밀번호를 입력하세요", - "totpAddFailed": "인증자 추가에 실패했습니다.", - "totpAddScanInstructions": "추가 인증 수단으로 이 QR 코드를 스캔하세요. 기존 인증 수단도 계속 사용할 수 있습니다.", - "totpAddSuccess": "추가 인증자가 등록되었습니다", - "disable": "비활성화", - "enable": "활성화", - "setupTotp": "TOTP 설정", - "qrCode": "QR 코드", - "totpInstructions": "QR 코드를 스캔하거나 인증기 앱에 비밀 키를 입력한 후 6자리 코드를 입력하세요", - "totpCodePlaceholder": "000000", - "verify": "확인", "changePassword": "비밀번호 변경", "currentPasswordLabel": "현재 비밀번호", "currentPasswordPlaceholder": "현재 비밀번호", @@ -2638,34 +2609,7 @@ "roleUser": "사용자", "authMethodDual": "이중 인증", "authMethodOidc": "OIDC", - "totpSetupFailed": "TOTP 설정 시작 실패", - "totpEnter6Digits": "6자리 코드 입력", - "totpEnabledSuccess": "2단계 인증이 활성화되었습니다.", - "totpInvalidCode": "유효하지 않은 코드입니다. 다시 시도하세요.", - "totpDisableInputRequired": "TOTP 코드 또는 비밀번호를 입력하세요.", - "totpDisabledSuccess": "2단계 인증이 비활성화되었습니다.", - "totpDisableFailed": "2FA 비활성화 실패", - "totpDisableTitle": "2FA 비활성화", - "totpDisablePlaceholder": "TOTP 코드 또는 비밀번호 입력", - "totpDisableConfirm": "2FA 비활성화", - "totpContinueVerify": "인증 계속하기", - "totpVerifyTitle": "코드 확인", - "totpBackupTitle": "백업 코드", - "totpDownloadBackup": "백업 코드 다운로드", - "passkeys": "패스키", - "passkeysDesc": "WebAuthn/FIDO2 자격 증명을 사용하여 비밀번호 없이 로그인하세요.", - "passkeyName": "패스키 이름", - "passkeyUvPreferred": "선호", - "passkeyUvRequired": "필수", - "passkeyUvDiscouraged": "권장 안 함", - "addPasskey": "패스키 추가", - "noPasskeys": "등록된 패스키가 없습니다.", - "passkeyAdded": "패스키가 추가되었습니다.", - "passkeyAddFailed": "패스키 추가 실패", - "passkeyDeleted": "패스키가 삭제되었습니다.", - "passkeyDeleteFailed": "패스키 삭제 실패", "done": "완료", - "secretCopied": "시크릿이 클립보드에 복사됨", "apiKeyNameRequired": "API 키 이름이 필요합니다", "apiKeyCreated": "API 키 \"{{name}}\" 생성됨", "apiKeyCreateFailed": "API 키 생성 실패", diff --git a/src/ui/locales/translated/nl_NL.json b/src/ui/locales/translated/nl_NL.json index 35dbfbd2a..433d7cb18 100644 --- a/src/ui/locales/translated/nl_NL.json +++ b/src/ui/locales/translated/nl_NL.json @@ -1592,8 +1592,6 @@ "hasAccount": "Heb je al een account?", "twoFactorAuth": "Tweefactorauthenticatie", "enterCode": "Voer de verificatiecode in.", - "backupCode": "Of gebruik de back-upcode.", - "verifyCode": "Code verifiëren", "redirectingToApp": "Doorverwijzen naar app...", "sshAuthenticationRequired": "SSH-authenticatie vereist", "sshNoKeyboardInteractive": "Toetsenbord-interactieve authenticatie niet beschikbaar", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "Authenticatie uitgeschakeld", "authenticationDisabledDesc": "Alle authenticatiemethoden zijn momenteel uitgeschakeld. Neem contact op met uw beheerder.", "passwordLoginDisabledDesc": "Inloggen met wachtwoord is uitgeschakeld. Gebruik een toegangscode of een externe authenticatieprovider.", - "signInWithPasskey": "Aanmelden met wachtwoord", - "passkeyLoginFailed": "Aanmelden met wachtwoord mislukt", "attemptsRemaining": "{{count}} resterende pogingen", "confirmResetDataWipe": "Dit account is sinds de upgrade van de encryptie niet meer gebruikt, waardoor de opgeslagen gegevens niet kunnen worden hersteld zonder het oude wachtwoord. Door het wachtwoord opnieuw in te stellen, worden de hosts, inloggegevens en codefragmenten permanent verwijderd. Doorgaan?" }, @@ -2065,7 +2061,6 @@ "saving": "Besparing...", "updateRegistrationFailed": "Het bijwerken van de registratie-instellingen is mislukt.", "updatePasswordLoginFailed": "Het bijwerken van de wachtwoordinstellingen is mislukt.", - "cannotDisablePasswordLoginWithTotp": "Het is niet mogelijk om wachtwoordaanmelding uit te schakelen wanneer tweefactorauthenticatie (2FA) is ingeschakeld voor een of meer gebruikers. Schakel eerst 2FA uit.", "updateOidcAutoProvisionFailed": "Het bijwerken van de OIDC-automatische provisioneringsinstelling is mislukt.", "updateOidcSilentLoginDefaultFailed": "Het bijwerken van de instelling voor stille OIDC-aanmelding is mislukt.", "updatePasswordResetFailed": "Het bijwerken van de instellingen voor het opnieuw instellen van het wachtwoord is mislukt.", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "Wachtwoord gereset. De versleutelde gegevens van de gebruiker zijn gewist.", "resetPasswordFailed": "Wachtwoord opnieuw instellen mislukt", "resetPasswordConfirmWipe": "{{username}} heeft niet meer ingelogd sinds de upgrade van de encryptie, waardoor hun gegevens niet kunnen worden hersteld. Door nu opnieuw in te stellen, worden hun hosts, inloggegevens en codefragmenten verwijderd. Doorgaan?", - "totpSectionTitle": "Tweefactorauthenticatie", - "totpStatusEnabled": "TOTP is ingeschakeld voor deze gebruiker.", - "totpStatusDisabled": "TOTP is niet ingeschakeld voor deze gebruiker.", - "disableTotp": "Uitzetten", - "disableTotpConfirm": "Schakel tweefactorauthenticatie uit voor {{username}}? Zij kunnen dan inloggen met alleen hun wachtwoord.", - "totpDisabledSuccess": "TOTP uitgeschakeld", - "totpDisableFailed": "TOTP uitschakelen is mislukt.", "manageApiKeys": "API-sleutels", "apiKeyNamePlaceholder": "Sleutelnaam", "apiKeyCopyNotice": "Kopieer deze sleutel nu, hij wordt niet meer weergegeven.", @@ -2576,23 +2564,6 @@ "settingsUpdates": "Updates", "disableUpdateChecks": "Schakel updatecontroles uit", "disableUpdateChecksDesc": "Stop met controleren op updates.", - "totpAuthenticator": "TOTP-authenticator", - "totpEnabled": "Tweefactorauthenticatie (2FA) is ingeschakeld.", - "totpDisabled": "Voeg extra inlogbeveiliging toe.", - "totpAddAuthenticator": "Apparaat toevoegen", - "totpAddTitle": "Authenticatorapparaat toevoegen", - "totpAddDescription": "Bevestig uw identiteit om een andere authenticatie-app te registreren met dezelfde beveiligde TOTP-sleutel.", - "totpAddInputRequired": "Voer uw TOTP-code of wachtwoord in.", - "totpAddFailed": "Authenticator toevoegen mislukt", - "totpAddScanInstructions": "Scan deze QR-code met de extra authenticatie-app. Bestaande authenticatie-apps blijven gewoon werken.", - "totpAddSuccess": "Extra authenticatie-app geregistreerd", - "disable": "Uitzetten", - "enable": "Inschakelen", - "setupTotp": "TOTP instellen", - "qrCode": "QR-code", - "totpInstructions": "Scan de QR-code of voer het geheime wachtwoord in uw authenticatie-app in en voer vervolgens de 6-cijferige code in.", - "totpCodePlaceholder": "000000", - "verify": "Verifiëren", "changePassword": "Wachtwoord wijzigen", "currentPasswordLabel": "Huidig wachtwoord", "currentPasswordPlaceholder": "Huidig wachtwoord", @@ -2638,34 +2609,7 @@ "roleUser": "Gebruiker", "authMethodDual": "Dubbele authenticatie", "authMethodOidc": "OIDC", - "totpSetupFailed": "Het starten van de TOTP-configuratie is mislukt.", - "totpEnter6Digits": "Voer een 6-cijferige code in", - "totpEnabledSuccess": "Tweefactorauthenticatie ingeschakeld", - "totpInvalidCode": "Ongeldige code, probeer het opnieuw.", - "totpDisableInputRequired": "Voer uw TOTP-code of wachtwoord in.", - "totpDisabledSuccess": "Tweefactorauthenticatie uitgeschakeld", - "totpDisableFailed": "Het uitschakelen van 2FA is mislukt.", - "totpDisableTitle": "Schakel 2FA uit", - "totpDisablePlaceholder": "Voer de TOTP-code of het wachtwoord in.", - "totpDisableConfirm": "Schakel 2FA uit", - "totpContinueVerify": "Doorgaan met verifiëren", - "totpVerifyTitle": "Code verifiëren", - "totpBackupTitle": "Back-upcodes", - "totpDownloadBackup": "Back-upcodes downloaden", - "passkeys": "Wachtwoorden", - "passkeysDesc": "Gebruik WebAuthn/FIDO2-gegevens om zonder wachtwoord in te loggen.", - "passkeyName": "Wachtwoordnaam", - "passkeyUvPreferred": "Voorkeur", - "passkeyUvRequired": "Vereist", - "passkeyUvDiscouraged": "Ontmoedigd", - "addPasskey": "Voeg wachtwoord toe", - "noPasskeys": "Er zijn geen wachtwoorden geregistreerd.", - "passkeyAdded": "Wachtwoord toegevoegd", - "passkeyAddFailed": "Wachtwoord toevoegen mislukt", - "passkeyDeleted": "Wachtwoord verwijderd", - "passkeyDeleteFailed": "Het verwijderen van de toegangscode is mislukt.", "done": "Klaar", - "secretCopied": "Geheim gekopieerd naar het klembord", "apiKeyNameRequired": "Sleutelnaam is vereist", "apiKeyCreated": "API-sleutel \"{{name}}\" aangemaakt", "apiKeyCreateFailed": "Het aanmaken van de API-sleutel is mislukt.", diff --git a/src/ui/locales/translated/no_NO.json b/src/ui/locales/translated/no_NO.json index 002b423d0..d52f9ae74 100644 --- a/src/ui/locales/translated/no_NO.json +++ b/src/ui/locales/translated/no_NO.json @@ -1592,8 +1592,6 @@ "hasAccount": "Har du allerede en konto?", "twoFactorAuth": "Tofaktorautentisering", "enterCode": "Skriv inn bekreftelseskoden", - "backupCode": "Eller bruk reservekoden", - "verifyCode": "Bekreft kode", "redirectingToApp": "Omdirigerer til appen...", "sshAuthenticationRequired": "SSH-autentisering kreves", "sshNoKeyboardInteractive": "Tastaturinteraktiv autentisering er ikke tilgjengelig", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "Autentisering deaktivert", "authenticationDisabledDesc": "Alle autentiseringsmetoder er for øyeblikket deaktivert. Ta kontakt med administratoren din.", "passwordLoginDisabledDesc": "Passordpålogging er deaktivert. Bruk en passordnøkkel eller en ekstern autentiseringsleverandør.", - "signInWithPasskey": "Logg inn med passord", - "passkeyLoginFailed": "Innlogging med passord mislyktes", "attemptsRemaining": "{{count}} forsøk igjen", "confirmResetDataWipe": "Denne kontoen har ikke vært logget inn siden krypteringsoppgraderingen, så de lagrede dataene kan ikke gjenopprettes uten det gamle passordet. Tilbakestilling vil slette vertene, påloggingsinformasjonen og kodebitene permanent. Vil du fortsette?" }, @@ -2065,7 +2061,6 @@ "saving": "Lagrer...", "updateRegistrationFailed": "Kunne ikke oppdatere registreringsinnstillingene", "updatePasswordLoginFailed": "Kunne ikke oppdatere passordinnstillingen for pålogging", - "cannotDisablePasswordLoginWithTotp": "Kan ikke deaktivere passordpålogging mens 2FA er aktivert for én eller flere brukere. Deaktiver 2FA først.", "updateOidcAutoProvisionFailed": "Kunne ikke oppdatere OIDC-innstillingen for automatisk provisjonering", "updateOidcSilentLoginDefaultFailed": "Kunne ikke oppdatere innstillingen for stille OIDC-pålogging", "updatePasswordResetFailed": "Kunne ikke oppdatere innstillingen for tilbakestilling av passord", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "Tilbakestilling av passord. Brukerens krypterte data ble slettet.", "resetPasswordFailed": "Kunne ikke tilbakestille passordet", "resetPasswordConfirmWipe": "{{username}} har ikke logget inn siden krypteringsoppgraderingen, så dataene deres kan ikke gjenopprettes. Tilbakestilling nå vil slette vertene, påloggingsinformasjonen og kodebitene deres. Vil du fortsette?", - "totpSectionTitle": "Tofaktorautentisering", - "totpStatusEnabled": "TOTP er aktivert for denne brukeren", - "totpStatusDisabled": "TOTP er ikke aktivert for denne brukeren", - "disableTotp": "Deaktiver", - "disableTotpConfirm": "Deaktiver tofaktorautentisering for {{username}}? De vil kunne logge inn med bare passordet sitt.", - "totpDisabledSuccess": "TOTP deaktivert", - "totpDisableFailed": "Kunne ikke deaktivere TOTP", "manageApiKeys": "API-nøkler", "apiKeyNamePlaceholder": "Nøkkelnavn", "apiKeyCopyNotice": "Kopier denne nøkkelen nå, den vil ikke vises igjen.", @@ -2576,23 +2564,6 @@ "settingsUpdates": "Oppdateringer", "disableUpdateChecks": "Deaktiver oppdateringssjekker", "disableUpdateChecksDesc": "Slutt å se etter oppdateringer", - "totpAuthenticator": "TOTP-autentisering", - "totpEnabled": "2FA er aktivert", - "totpDisabled": "Legg til ekstra sikkerhet for pålogging", - "totpAddAuthenticator": "Legg til enhet", - "totpAddTitle": "Legg til autentiseringsenhet", - "totpAddDescription": "Bekreft identiteten din for å registrere en annen autentiseringsfunksjon med samme sikre TOTP-nøkkel.", - "totpAddInputRequired": "Skriv inn TOTP-koden eller passordet ditt", - "totpAddFailed": "Kunne ikke legge til autentiseringsfunksjonen", - "totpAddScanInstructions": "Skann denne QR-koden med den ekstra autentiseringsfunksjonen. Eksisterende autentiseringsfunksjoner vil fortsette å fungere.", - "totpAddSuccess": "Ekstra autentiseringsfunksjon registrert", - "disable": "Deaktiver", - "enable": "Aktiver", - "setupTotp": "Konfigurer TOTP", - "qrCode": "QR-kode", - "totpInstructions": "Skann QR-koden eller skriv inn hemmeligheten i autentiseringsappen din, og skriv deretter inn den 6-sifrede koden", - "totpCodePlaceholder": "000000", - "verify": "Verifisere", "changePassword": "Endre passord", "currentPasswordLabel": "Nåværende passord", "currentPasswordPlaceholder": "Nåværende passord", @@ -2638,34 +2609,7 @@ "roleUser": "Bruker", "authMethodDual": "Dobbel autentisering", "authMethodOidc": "OIDC", - "totpSetupFailed": "Kunne ikke starte TOTP-oppsettet", - "totpEnter6Digits": "Skriv inn en 6-sifret kode", - "totpEnabledSuccess": "Tofaktorautentisering aktivert", - "totpInvalidCode": "Ugyldig kode, prøv på nytt", - "totpDisableInputRequired": "Skriv inn TOTP-koden eller passordet ditt", - "totpDisabledSuccess": "Tofaktorautentisering deaktivert", - "totpDisableFailed": "Kunne ikke deaktivere 2FA", - "totpDisableTitle": "Deaktiver 2FA", - "totpDisablePlaceholder": "Skriv inn TOTP-kode eller passord", - "totpDisableConfirm": "Deaktiver 2FA", - "totpContinueVerify": "Fortsett å bekrefte", - "totpVerifyTitle": "Bekreft kode", - "totpBackupTitle": "Sikkerhetskopieringskoder", - "totpDownloadBackup": "Last ned sikkerhetskoder", - "passkeys": "Passord", - "passkeysDesc": "Bruk WebAuthn/FIDO2-legitimasjon for å logge på uten passord", - "passkeyName": "Navn på passord", - "passkeyUvPreferred": "Foretrukket", - "passkeyUvRequired": "Obligatorisk", - "passkeyUvDiscouraged": "Motløs", - "addPasskey": "Legg til tilgangsnøkkel", - "noPasskeys": "Ingen passord registrert.", - "passkeyAdded": "Tilgangsnøkkel lagt til", - "passkeyAddFailed": "Kunne ikke legge til passord", - "passkeyDeleted": "Tilgangsnøkkel slettet", - "passkeyDeleteFailed": "Kunne ikke slette passordet", "done": "Ferdig", - "secretCopied": "Hemmelighet kopiert til utklippstavlen", "apiKeyNameRequired": "Nøkkelnavn er obligatorisk", "apiKeyCreated": "API-nøkkelen «{{name}}» ble opprettet", "apiKeyCreateFailed": "Kunne ikke opprette API-nøkkel", diff --git a/src/ui/locales/translated/pl_PL.json b/src/ui/locales/translated/pl_PL.json index f494bb59d..c43f1d21f 100644 --- a/src/ui/locales/translated/pl_PL.json +++ b/src/ui/locales/translated/pl_PL.json @@ -1592,8 +1592,6 @@ "hasAccount": "Masz już konto?", "twoFactorAuth": "Uwierzytelnianie dwuskładnikowe", "enterCode": "Wprowadź kod weryfikacyjny", - "backupCode": "Lub użyj kodu zapasowego", - "verifyCode": "Zweryfikuj kod", "redirectingToApp": "Przekierowywanie do aplikacji...", "sshAuthenticationRequired": "Wymagane uwierzytelnienie SSH", "sshNoKeyboardInteractive": "Uwierzytelnianie za pomocą klawiatury jest niedostępne", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "Uwierzytelnianie wyłączone", "authenticationDisabledDesc": "Wszystkie metody uwierzytelniania są obecnie wyłączone. Skontaktuj się z administratorem.", "passwordLoginDisabledDesc": "Logowanie hasłem jest wyłączone. Użyj klucza dostępu lub zewnętrznego dostawcy uwierzytelniania.", - "signInWithPasskey": "Zaloguj się za pomocą klucza dostępu", - "passkeyLoginFailed": "Nie udało się zalogować za pomocą klucza dostępu", "attemptsRemaining": "Pozostało {{count}} prób", "confirmResetDataWipe": "To konto nie logowało się od czasu aktualizacji szyfrowania, więc bez starego hasła nie da się odzyskać zapisanych na nim danych. Zresetowanie trwale usunie jego hosty, poświadczenia i snippety. Kontynuować?" }, @@ -2065,7 +2061,6 @@ "saving": "Zapisywanie...", "updateRegistrationFailed": "Nie udało się zaktualizować ustawień rejestracji", "updatePasswordLoginFailed": "Nie udało się zaktualizować ustawień logowania hasłem", - "cannotDisablePasswordLoginWithTotp": "Nie można wyłączyć logowania hasłem, gdy 2FA jest włączone dla jednego lub większej liczby użytkowników. Najpierw wyłącz 2FA.", "updateOidcAutoProvisionFailed": "Nie udało się zaktualizować ustawienia automatycznego tworzenia kont OIDC", "updateOidcSilentLoginDefaultFailed": "Nie udało się zaktualizować ustawień cichego logowania OIDC", "updatePasswordResetFailed": "Nie udało się zaktualizować ustawienia resetowania hasła", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "Zresetowano hasło. Zaszyfrowane dane użytkownika zostały usunięte.", "resetPasswordFailed": "Nie udało się zresetować hasła", "resetPasswordConfirmWipe": "{{username}} nie logował się od czasu aktualizacji szyfrowania, więc jego danych nie da się odzyskać. Zresetowanie teraz usunie jego hosty, poświadczenia i snippety. Kontynuować?", - "totpSectionTitle": "Uwierzytelnianie dwuskładnikowe", - "totpStatusEnabled": "TOTP jest włączony dla tego użytkownika", - "totpStatusDisabled": "TOTP nie jest włączony dla tego użytkownika", - "disableTotp": "Wyłącz", - "disableTotpConfirm": "Wyłączyć uwierzytelnianie dwuskładnikowe dla {{username}}? Ten użytkownik będzie mógł logować się, podając tylko hasło.", - "totpDisabledSuccess": "Wyłączono TOTP", - "totpDisableFailed": "Nie udało się wyłączyć TOTP", "manageApiKeys": "Klucze API", "apiKeyNamePlaceholder": "Nazwa klucza", "apiKeyCopyNotice": "Skopiuj ten klucz teraz, nie zostanie pokazany ponownie.", @@ -2576,23 +2564,6 @@ "settingsUpdates": "Aktualizacje", "disableUpdateChecks": "Wyłącz sprawdzanie aktualizacji", "disableUpdateChecksDesc": "Przestań sprawdzać aktualizacje", - "totpAuthenticator": "Aplikacja TOTP", - "totpEnabled": "Włączono uwierzytelnianie dwuskładnikowe", - "totpDisabled": "Dodaj dodatkowe zabezpieczenia logowania", - "totpAddAuthenticator": "Dodaj urządzenie", - "totpAddTitle": "Dodaj urządzenie uwierzytelniające", - "totpAddDescription": "Potwierdź swoją tożsamość, aby zarejestrować inny podmiot uwierzytelniający z tym samym bezpiecznym kluczem TOTP.", - "totpAddInputRequired": "Wprowadź swój kod TOTP lub hasło", - "totpAddFailed": "Nie udało się dodać uwierzytelniacza", - "totpAddScanInstructions": "Zeskanuj ten kod QR za pomocą dodatkowego uwierzytelniacza. Istniejące uwierzytelniacze będą nadal działać.", - "totpAddSuccess": "Zarejestrowano dodatkowego uwierzytelniacza", - "disable": "Wyłącz", - "enable": "Włącz", - "setupTotp": "Skonfiguruj TOTP", - "qrCode": "Kod QR", - "totpInstructions": "Zeskanuj kod QR lub wprowadź sekret w aplikacji uwierzytelniającej, a następnie wpisz 6-cyfrowy kod", - "totpCodePlaceholder": "000000", - "verify": "Zweryfikuj", "changePassword": "Zmień hasło", "currentPasswordLabel": "Aktualne hasło", "currentPasswordPlaceholder": "Aktualne hasło", @@ -2638,34 +2609,7 @@ "roleUser": "Użytkownik", "authMethodDual": "Podwójne uwierzytelnianie", "authMethodOidc": "OIDC", - "totpSetupFailed": "Nie udało się uruchomić konfiguracji TOTP", - "totpEnter6Digits": "Wprowadź 6-cyfrowy kod", - "totpEnabledSuccess": "Włączono uwierzytelnianie dwuskładnikowe", - "totpInvalidCode": "Nieprawidłowy kod, spróbuj ponownie", - "totpDisableInputRequired": "Wprowadź kod TOTP lub hasło", - "totpDisabledSuccess": "Wyłączono uwierzytelnianie dwuskładnikowe", - "totpDisableFailed": "Nie udało się wyłączyć uwierzytelniania dwuskładnikowego", - "totpDisableTitle": "Wyłącz 2FA", - "totpDisablePlaceholder": "Wprowadź kod TOTP lub hasło", - "totpDisableConfirm": "Wyłącz 2FA", - "totpContinueVerify": "Kontynuuj weryfikację", - "totpVerifyTitle": "Zweryfikuj kod", - "totpBackupTitle": "Kody zapasowe", - "totpDownloadBackup": "Pobierz kody zapasowe", - "passkeys": "Klucze dostępu", - "passkeysDesc": "Używaj poświadczeń WebAuthn/FIDO2, aby logować się bez hasła", - "passkeyName": "Nazwa klucza dostępu", - "passkeyUvPreferred": "Preferowane", - "passkeyUvRequired": "Wymagane", - "passkeyUvDiscouraged": "Odradzane", - "addPasskey": "Dodaj klucz dostępu", - "noPasskeys": "Nie zarejestrowano żadnych kluczy dostępu.", - "passkeyAdded": "Dodano klucz dostępu", - "passkeyAddFailed": "Nie udało się dodać klucza dostępu", - "passkeyDeleted": "Usunięto klucz dostępu", - "passkeyDeleteFailed": "Nie udało się usunąć klucza dostępu", "done": "Gotowe", - "secretCopied": "Skopiowano sekret do schowka", "apiKeyNameRequired": "Nazwa klucza jest wymagana", "apiKeyCreated": "Utworzono klucz API „{{name}}”", "apiKeyCreateFailed": "Nie udało się utworzyć klucza API", diff --git a/src/ui/locales/translated/pt_BR.json b/src/ui/locales/translated/pt_BR.json index 8cce31bb2..3f90264c6 100644 --- a/src/ui/locales/translated/pt_BR.json +++ b/src/ui/locales/translated/pt_BR.json @@ -1592,8 +1592,6 @@ "hasAccount": "Já tem uma conta?", "twoFactorAuth": "Autenticação de dois fatores", "enterCode": "Insira o código de verificação", - "backupCode": "Ou use o código de backup.", - "verifyCode": "Código de verificação", "redirectingToApp": "Redirecionando para o aplicativo...", "sshAuthenticationRequired": "Autenticação SSH necessária", "sshNoKeyboardInteractive": "Autenticação interativa por teclado indisponível", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "Autenticação desativada", "authenticationDisabledDesc": "Todos os métodos de autenticação estão desativados no momento. Entre em contato com o administrador.", "passwordLoginDisabledDesc": "O login com senha está desativado. Use uma chave de acesso ou um provedor de autenticação externo.", - "signInWithPasskey": "Faça login com a chave de acesso.", - "passkeyLoginFailed": "Falha no login com a chave de acesso", "attemptsRemaining": "{{count}} tentativas restantes", "confirmResetDataWipe": "Esta conta não foi acessada desde a atualização da criptografia, portanto, seus dados armazenados não podem ser recuperados sem a senha antiga. A redefinição excluirá permanentemente seus hosts, credenciais e trechos de código. Continuar?" }, @@ -2065,7 +2061,6 @@ "saving": "Salvando...", "updateRegistrationFailed": "Falha ao atualizar as configurações de registro", "updatePasswordLoginFailed": "Falha ao atualizar as configurações de login da senha.", - "cannotDisablePasswordLoginWithTotp": "Não é possível desativar o login por senha enquanto a autenticação de dois fatores (2FA) estiver ativada para um ou mais usuários. Desative a 2FA primeiro.", "updateOidcAutoProvisionFailed": "Falha ao atualizar a configuração de provisionamento automático do OIDC", "updateOidcSilentLoginDefaultFailed": "Falha ao atualizar a configuração de login silencioso do OIDC", "updatePasswordResetFailed": "Falha ao atualizar as configurações de redefinição de senha", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "Redefinição de senha. Os dados criptografados do usuário foram apagados.", "resetPasswordFailed": "Falha ao redefinir a senha", "resetPasswordConfirmWipe": "{{username}} não fez login desde a atualização de criptografia, portanto, seus dados não podem ser recuperados. Redefinir agora excluirá seus hosts, credenciais e trechos de código. Continuar?", - "totpSectionTitle": "Autenticação de dois fatores", - "totpStatusEnabled": "O TOTP está ativado para este usuário.", - "totpStatusDisabled": "O TOTP não está habilitado para este usuário.", - "disableTotp": "Desativar", - "disableTotpConfirm": "Desativar autenticação de dois fatores para {{username}}? Eles poderão fazer login apenas com a senha.", - "totpDisabledSuccess": "TOTP desativado", - "totpDisableFailed": "Falha ao desativar o TOTP", "manageApiKeys": "Chaves de API", "apiKeyNamePlaceholder": "Nome da chave", "apiKeyCopyNotice": "Copie esta chave agora, ela não será exibida novamente.", @@ -2576,23 +2564,6 @@ "settingsUpdates": "Atualizações", "disableUpdateChecks": "Desativar verificações de atualização", "disableUpdateChecksDesc": "Pare de verificar se há atualizações.", - "totpAuthenticator": "Autenticador TOTP", - "totpEnabled": "A autenticação de dois fatores está ativada.", - "totpDisabled": "Adicione segurança extra ao login.", - "totpAddAuthenticator": "Adicionar dispositivo", - "totpAddTitle": "Adicionar dispositivo autenticador", - "totpAddDescription": "Confirme sua identidade para cadastrar outro autenticador com a mesma chave TOTP segura.", - "totpAddInputRequired": "Digite seu código TOTP ou senha.", - "totpAddFailed": "Falha ao adicionar autenticador", - "totpAddScanInstructions": "Escaneie este código QR com o autenticador adicional. Os autenticadores existentes continuarão funcionando.", - "totpAddSuccess": "Autenticador adicional cadastrado", - "disable": "Desativar", - "enable": "Habilitar", - "setupTotp": "Configurar TOTP", - "qrCode": "Código QR", - "totpInstructions": "Escaneie o código QR ou insira a senha no seu aplicativo autenticador e, em seguida, digite o código de 6 dígitos.", - "totpCodePlaceholder": "000000", - "verify": "Verificar", "changePassword": "Alterar a senha", "currentPasswordLabel": "Senha atual", "currentPasswordPlaceholder": "Senha atual", @@ -2638,34 +2609,7 @@ "roleUser": "Usuário", "authMethodDual": "Autenticação dupla", "authMethodOidc": "OIDC", - "totpSetupFailed": "Falha ao iniciar a configuração TOTP", - "totpEnter6Digits": "Digite um código de 6 dígitos", - "totpEnabledSuccess": "Autenticação de dois fatores ativada", - "totpInvalidCode": "Código inválido, tente novamente.", - "totpDisableInputRequired": "Digite seu código TOTP ou senha.", - "totpDisabledSuccess": "Autenticação de dois fatores desativada", - "totpDisableFailed": "Falha ao desativar a autenticação de dois fatores (2FA).", - "totpDisableTitle": "Desative a autenticação de dois fatores (2FA).", - "totpDisablePlaceholder": "Digite o código TOTP ou a senha.", - "totpDisableConfirm": "Desative a autenticação de dois fatores (2FA).", - "totpContinueVerify": "Continuar a verificar", - "totpVerifyTitle": "Código de verificação", - "totpBackupTitle": "Códigos de backup", - "totpDownloadBackup": "Baixar códigos de backup", - "passkeys": "Senhas", - "passkeysDesc": "Use as credenciais WebAuthn/FIDO2 para fazer login sem senha.", - "passkeyName": "Nome da chave de acesso", - "passkeyUvPreferred": "Preferido", - "passkeyUvRequired": "Obrigatório", - "passkeyUvDiscouraged": "Desencorajado", - "addPasskey": "Adicionar chave de acesso", - "noPasskeys": "Nenhuma chave de acesso registrada.", - "passkeyAdded": "Senha adicionada", - "passkeyAddFailed": "Falha ao adicionar a chave de acesso.", - "passkeyDeleted": "Senha excluída", - "passkeyDeleteFailed": "Falha ao excluir a chave de acesso.", "done": "Feito", - "secretCopied": "Segredo copiado para a área de transferência", "apiKeyNameRequired": "É necessário nomear a chave.", "apiKeyCreated": "Chave de API \"{{name}}\" criada", "apiKeyCreateFailed": "Falha ao criar a chave da API", diff --git a/src/ui/locales/translated/pt_PT.json b/src/ui/locales/translated/pt_PT.json index 0710497eb..59f87ed8f 100644 --- a/src/ui/locales/translated/pt_PT.json +++ b/src/ui/locales/translated/pt_PT.json @@ -1592,8 +1592,6 @@ "hasAccount": "Já tem uma conta?", "twoFactorAuth": "Autenticação de dois fatores", "enterCode": "Introduza o código de verificação", - "backupCode": "Ou utilize código de backup", - "verifyCode": "Verificar Código", "redirectingToApp": "A redirecionar para a aplicação...", "sshAuthenticationRequired": "Autenticação SSH necessária", "sshNoKeyboardInteractive": "Autenticação por teclado interativo indisponível", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "Autenticação desativada", "authenticationDisabledDesc": "Todos os métodos de autenticação estão atualmente desativados. Contacte o seu administrador.", "passwordLoginDisabledDesc": "O início de sessão com palavra-passe está desativado. Utilize uma passkey ou um fornecedor de autenticação externo.", - "signInWithPasskey": "Iniciar sessão com passkey", - "passkeyLoginFailed": "Falha no início de sessão com passkey", "attemptsRemaining": "{{count}} tentativas restantes", "confirmResetDataWipe": "Esta conta não iniciou sessão desde a atualização da encriptação, pelo que os dados armazenados não podem ser recuperados sem a palavra-passe antiga. A reposição irá eliminar permanentemente os seus hosts, credenciais e snippets. Continuar?" }, @@ -2065,7 +2061,6 @@ "saving": "A guardar...", "updateRegistrationFailed": "Falha ao atualizar a definição de registo", "updatePasswordLoginFailed": "Falha ao atualizar a definição de início de sessão por palavra-passe", - "cannotDisablePasswordLoginWithTotp": "Não é possível desativar o início de sessão por palavra-passe enquanto a 2FA estiver ativa para um ou mais utilizadores. Desative a 2FA primeiro.", "updateOidcAutoProvisionFailed": "Falha ao atualizar a definição de provisionamento automático OIDC", "updateOidcSilentLoginDefaultFailed": "Falha ao atualizar a definição de início de sessão silencioso OIDC", "updatePasswordResetFailed": "Falha ao atualizar a definição de reposição de palavra-passe", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "Palavra-passe redefinida. Os dados encriptados do utilizador foram eliminados.", "resetPasswordFailed": "Falha ao redefinir a palavra-passe", "resetPasswordConfirmWipe": "{{username}} não iniciou sessão desde a atualização da encriptação, pelo que os seus dados não podem ser recuperados. Redefinir agora eliminará as suas máquinas, credenciais e fragmentos. Continuar?", - "totpSectionTitle": "Autenticação de dois fatores", - "totpStatusEnabled": "TOTP está ativado para este utilizador", - "totpStatusDisabled": "TOTP não está ativado para este utilizador", - "disableTotp": "Desativar", - "disableTotpConfirm": "Desativar a autenticação de dois fatores para {{username}}? Poderá iniciar sessão apenas com a sua palavra-passe.", - "totpDisabledSuccess": "TOTP desativado", - "totpDisableFailed": "Falha ao desativar TOTP", "manageApiKeys": "Chaves de API", "apiKeyNamePlaceholder": "Nome da chave", "apiKeyCopyNotice": "Copie esta chave agora, não será mostrada novamente.", @@ -2576,23 +2564,6 @@ "settingsUpdates": "Atualizações", "disableUpdateChecks": "Desativar verificações de atualizações", "disableUpdateChecksDesc": "Parar de verificar atualizações", - "totpAuthenticator": "Autenticador TOTP", - "totpEnabled": "2FA está ativada", - "totpDisabled": "Adicionar segurança extra de início de sessão", - "totpAddAuthenticator": "Adicionar dispositivo", - "totpAddTitle": "Adicionar dispositivo autenticador", - "totpAddDescription": "Confirme a sua identidade para registar outro autenticador com a mesma chave TOTP segura.", - "totpAddInputRequired": "Introduza o seu código TOTP ou palavra-passe.", - "totpAddFailed": "Falha ao adicionar autenticador", - "totpAddScanInstructions": "Digitalize este código QR com o autenticador adicional. Os autenticadores existentes continuarão a funcionar.", - "totpAddSuccess": "Autenticador adicional registado", - "disable": "Desativar", - "enable": "Ativar", - "setupTotp": "Configurar TOTP", - "qrCode": "Código QR", - "totpInstructions": "Leia o código QR ou introduza o segredo na sua aplicação autenticadora e depois introduza o código de 6 dígitos", - "totpCodePlaceholder": "000000", - "verify": "Verificar", "changePassword": "Alterar palavra-passe", "currentPasswordLabel": "Palavra-passe atual", "currentPasswordPlaceholder": "Palavra-passe atual", @@ -2638,34 +2609,7 @@ "roleUser": "Utilizador", "authMethodDual": "Autenticação Dupla", "authMethodOidc": "OIDC", - "totpSetupFailed": "Falha ao iniciar a configuração de TOTP", - "totpEnter6Digits": "Insira um código de 6 dígitos", - "totpEnabledSuccess": "Autenticação de dois fatores ativada", - "totpInvalidCode": "Código inválido, tente novamente", - "totpDisableInputRequired": "Insira o seu código TOTP ou palavra-passe", - "totpDisabledSuccess": "Autenticação de dois fatores desativada", - "totpDisableFailed": "Falha ao desativar 2FA", - "totpDisableTitle": "Desativar 2FA", - "totpDisablePlaceholder": "Insira o código TOTP ou palavra-passe", - "totpDisableConfirm": "Desativar 2FA", - "totpContinueVerify": "Continuar para Verificação", - "totpVerifyTitle": "Verificar Código", - "totpBackupTitle": "Códigos de Backup", - "totpDownloadBackup": "Descarregar Códigos de Backup", - "passkeys": "Passkeys", - "passkeysDesc": "Utilize credenciais WebAuthn/FIDO2 para iniciar sessão sem palavra-passe", - "passkeyName": "Nome da passkey", - "passkeyUvPreferred": "Preferencial", - "passkeyUvRequired": "Obrigatório", - "passkeyUvDiscouraged": "Desaconselhado", - "addPasskey": "Adicionar Passkey", - "noPasskeys": "Nenhuma passkey registada.", - "passkeyAdded": "Passkey adicionada", - "passkeyAddFailed": "Falha ao adicionar passkey", - "passkeyDeleted": "Passkey eliminada", - "passkeyDeleteFailed": "Falha ao eliminar chave de acesso", "done": "Concluído", - "secretCopied": "Segredo copiado para a área de transferência", "apiKeyNameRequired": "Nome da chave é obrigatório", "apiKeyCreated": "Chave de API \"{{name}}\" criada", "apiKeyCreateFailed": "Falha ao criar chave de API", diff --git a/src/ui/locales/translated/ro_RO.json b/src/ui/locales/translated/ro_RO.json index ecea06a42..093933c55 100644 --- a/src/ui/locales/translated/ro_RO.json +++ b/src/ui/locales/translated/ro_RO.json @@ -1592,8 +1592,6 @@ "hasAccount": "Ai deja un cont?", "twoFactorAuth": "Autentificare cu doi factori", "enterCode": "Introduceți codul de verificare", - "backupCode": "Sau folosește codul de rezervă", - "verifyCode": "Verificați codul", "redirectingToApp": "Redirecționare către aplicație...", "sshAuthenticationRequired": "Autentificare SSH necesară", "sshNoKeyboardInteractive": "Autentificare interactivă prin tastatură indisponibilă", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "Autentificare dezactivată", "authenticationDisabledDesc": "Toate metodele de autentificare sunt dezactivate în prezent. Vă rugăm să contactați administratorul.", "passwordLoginDisabledDesc": "Conectarea cu parolă este dezactivată. Folosiți o cheie de acces sau un furnizor de autentificare extern.", - "signInWithPasskey": "Conectați-vă cu cheia de acces", - "passkeyLoginFailed": "Conectarea cu parola a eșuat", "attemptsRemaining": "{{count}} încercări rămase", "confirmResetDataWipe": "Acest cont nu s-a mai conectat de la actualizarea criptării, așadar datele sale stocate nu pot fi recuperate fără vechea parolă. Resetarea va șterge definitiv gazdele, acreditările și fragmentele de date. Continuați?" }, @@ -2065,7 +2061,6 @@ "saving": "Economisire...", "updateRegistrationFailed": "Nu s-a putut actualiza setările de înregistrare", "updatePasswordLoginFailed": "Setările de conectare cu parola nu au putut fi actualizate.", - "cannotDisablePasswordLoginWithTotp": "Nu se poate dezactiva conectarea cu parolă cât timp 2FA este activat pentru unul sau mai mulți utilizatori. Dezactivați mai întâi 2FA.", "updateOidcAutoProvisionFailed": "Nu s-a putut actualiza setarea de furnizare automată OIDC.", "updateOidcSilentLoginDefaultFailed": "Nu s-a putut actualiza setarea de conectare silențioasă OIDC", "updatePasswordResetFailed": "Setările de resetare a parolei nu au putut fi actualizate.", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "Resetare parolă. Datele criptate ale utilizatorului au fost șterse.", "resetPasswordFailed": "Resetarea parolei a eșuat", "resetPasswordConfirmWipe": "{{username}} nu s-a conectat de la actualizarea criptării, prin urmare datele sale nu pot fi recuperate. Resetarea acum va șterge gazdele, acreditările și fragmentele de date. Continuați?", - "totpSectionTitle": "Autentificare cu doi factori", - "totpStatusEnabled": "TOTP este activat pentru acest utilizator", - "totpStatusDisabled": "TOTP nu este activat pentru acest utilizator", - "disableTotp": "Dezactivare", - "disableTotpConfirm": "Dezactivați autentificarea cu doi factori pentru {{username}}? Aceștia se vor putea conecta doar cu parola.", - "totpDisabledSuccess": "TOTP dezactivat", - "totpDisableFailed": "Nu s-a putut dezactiva TOTP", "manageApiKeys": "Chei API", "apiKeyNamePlaceholder": "Numele cheii", "apiKeyCopyNotice": "Copiază această cheie acum, nu va mai fi afișată.", @@ -2576,23 +2564,6 @@ "settingsUpdates": "Actualizări", "disableUpdateChecks": "Dezactivați verificările de actualizare", "disableUpdateChecksDesc": "Opriți verificarea actualizărilor", - "totpAuthenticator": "Autentificator TOTP", - "totpEnabled": "2FA este activat", - "totpDisabled": "Adăugați securitate suplimentară la conectare", - "totpAddAuthenticator": "Adăugați dispozitiv", - "totpAddTitle": "Adăugați un dispozitiv de autentificare", - "totpAddDescription": "Confirmați-vă identitatea pentru a înscrie un alt autentificator cu aceeași cheie TOTP securizată.", - "totpAddInputRequired": "Introduceți codul sau parola TOTP", - "totpAddFailed": "Nu s-a putut adăuga autentificatorul", - "totpAddScanInstructions": "Scanați acest cod QR cu ajutorul autentificatorului suplimentar. Autentificatoarele existente vor continua să funcționeze.", - "totpAddSuccess": "Autentificator suplimentar înscris", - "disable": "Dezactivare", - "enable": "Permite", - "setupTotp": "Configurați TOTP", - "qrCode": "Cod QR", - "totpInstructions": "Scanați codul QR sau introduceți codul secret în aplicația de autentificare, apoi introduceți codul de 6 cifre", - "totpCodePlaceholder": "000000", - "verify": "Verifica", "changePassword": "Schimbaţi parola", "currentPasswordLabel": "Parola actuală", "currentPasswordPlaceholder": "Parola actuală", @@ -2638,34 +2609,7 @@ "roleUser": "Utilizator", "authMethodDual": "Autentificare duală", "authMethodOidc": "OIDC", - "totpSetupFailed": "Nu s-a putut porni configurarea TOTP", - "totpEnter6Digits": "Introduceți un cod de 6 cifre", - "totpEnabledSuccess": "Autentificarea cu doi factori activată", - "totpInvalidCode": "Cod nevalid, vă rugăm să încercați din nou", - "totpDisableInputRequired": "Introduceți codul sau parola TOTP", - "totpDisabledSuccess": "Autentificarea cu doi factori este dezactivată", - "totpDisableFailed": "Nu s-a putut dezactiva 2FA", - "totpDisableTitle": "Dezactivați 2FA", - "totpDisablePlaceholder": "Introduceți codul TOTP sau parola", - "totpDisableConfirm": "Dezactivați 2FA", - "totpContinueVerify": "Continuați verificarea", - "totpVerifyTitle": "Verificați codul", - "totpBackupTitle": "Coduri de rezervă", - "totpDownloadBackup": "Descărcați coduri de rezervă", - "passkeys": "Chei de acces", - "passkeysDesc": "Folosește acreditările WebAuthn/FIDO2 pentru a te conecta fără parolă", - "passkeyName": "Numele cheii de acces", - "passkeyUvPreferred": "Preferat", - "passkeyUvRequired": "Necesar", - "passkeyUvDiscouraged": "Descurajat", - "addPasskey": "Adăugați o cheie de acces", - "noPasskeys": "Nicio cheie de acces înregistrată.", - "passkeyAdded": "Cheie de acces adăugată", - "passkeyAddFailed": "Nu s-a putut adăuga parola", - "passkeyDeleted": "Cheia de acces a fost ștearsă", - "passkeyDeleteFailed": "Nu s-a putut șterge parola", "done": "Făcut", - "secretCopied": "Secret copiat în clipboard", "apiKeyNameRequired": "Numele cheii este obligatoriu", "apiKeyCreated": "Cheia API „{{name}}” a fost creată", "apiKeyCreateFailed": "Nu s-a putut crea cheia API", diff --git a/src/ui/locales/translated/ru_RU.json b/src/ui/locales/translated/ru_RU.json index 97c887b23..f099e575a 100644 --- a/src/ui/locales/translated/ru_RU.json +++ b/src/ui/locales/translated/ru_RU.json @@ -1592,8 +1592,6 @@ "hasAccount": "Уже есть учётная запись?", "twoFactorAuth": "Двухфакторная аутентификация", "enterCode": "Введите проверочный код", - "backupCode": "Или используйте резервный код", - "verifyCode": "Проверить код", "redirectingToApp": "Перенаправление в приложение...", "sshAuthenticationRequired": "Требуется аутентификация SSH", "sshNoKeyboardInteractive": "Интерактивная аутентификация с клавиатурой недоступна", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "Аутентификация отключена", "authenticationDisabledDesc": "Все методы аутентификации в настоящее время отключены. Обратитесь к администратору.", "passwordLoginDisabledDesc": "Вход по паролю отключён. Используйте ключ доступа или внешнего провайдера аутентификации.", - "signInWithPasskey": "Войти с ключом доступа", - "passkeyLoginFailed": "Вход по ключу не удался", "attemptsRemaining": "Осталось попыток: {{count}}", "confirmResetDataWipe": "Эта учетная запись не входила с момента обновления шифрования, поэтому ее сохраненные данные не могут быть восстановлены без старого пароля. Сброс навсегда удалит ее хосты, учетные данные и сниппеты. Продолжить?" }, @@ -2065,7 +2061,6 @@ "saving": "Сохранение...", "updateRegistrationFailed": "Не удалось обновить настройки регистрации", "updatePasswordLoginFailed": "Не удалось обновить настройки входа по паролю", - "cannotDisablePasswordLoginWithTotp": "Нельзя отключить вход по паролю, если у одного или нескольких пользователей включена 2FA. Сначала отключите 2FA.", "updateOidcAutoProvisionFailed": "Не удалось обновить настройки автоматического создания пользователей через OIDC", "updateOidcSilentLoginDefaultFailed": "Не удалось обновить настройки автоматического входа через OIDC", "updatePasswordResetFailed": "Не удалось обновить настройки сброса пароля", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "Пароль сброшен. Зашифрованные данные пользователя удалены.", "resetPasswordFailed": "Не удалось сбросить пароль", "resetPasswordConfirmWipe": "{{username}} не входил в систему после обновления шифрования, поэтому его данные не могут быть восстановлены. При сбросе будут удалены его хосты, учётные данные и сниппеты. Продолжить?", - "totpSectionTitle": "Двухфакторная аутентификация", - "totpStatusEnabled": "TOTP включён для этого пользователя", - "totpStatusDisabled": "TOTP не включён для этого пользователя", - "disableTotp": "Отключить", - "disableTotpConfirm": "Отключить двухфакторную аутентификацию для {{username}}? После этого он сможет входить только с паролем.", - "totpDisabledSuccess": "TOTP отключён", - "totpDisableFailed": "Не удалось отключить TOTP", "manageApiKeys": "API-ключи", "apiKeyNamePlaceholder": "Имя ключа", "apiKeyCopyNotice": "Скопируйте этот ключ сейчас, он больше не будет показан.", @@ -2576,23 +2564,6 @@ "settingsUpdates": "Обновления", "disableUpdateChecks": "Отключить проверку обновлений", "disableUpdateChecksDesc": "Не проверять наличие обновлений", - "totpAuthenticator": "Аутентификатор TOTP", - "totpEnabled": "2FA включена", - "totpDisabled": "Добавить дополнительную защиту", - "totpAddAuthenticator": "Добавить устройство", - "totpAddTitle": "Добавить устройство аутентификации", - "totpAddDescription": "Подтвердите свою личность, чтобы зарегистрировать еще одного аутентификатора с тем же защищенным TOTP-ключом.", - "totpAddInputRequired": "Введите свой TOTP-код или пароль.", - "totpAddFailed": "Не удалось добавить аутентификатор.", - "totpAddScanInstructions": "Отсканируйте этот QR-код с помощью дополнительного аутентификатора. Существующие аутентификаторы продолжат работать.", - "totpAddSuccess": "Зарегистрирован дополнительный аутентификатор.", - "disable": "Отключить", - "enable": "Включить", - "setupTotp": "Настроить TOTP", - "qrCode": "QR-код", - "totpInstructions": "Сканируйте QR-код или введите секрет в аутентификаторе, затем введите 6-значный код", - "totpCodePlaceholder": "000000", - "verify": "Проверить", "changePassword": "Сменить пароль", "currentPasswordLabel": "Текущий пароль", "currentPasswordPlaceholder": "Текущий пароль", @@ -2638,34 +2609,7 @@ "roleUser": "Пользователь", "authMethodDual": "Двойная аутентификация", "authMethodOidc": "OIDC", - "totpSetupFailed": "Не удалось запустить настройку TOTP", - "totpEnter6Digits": "Введите 6-значный код", - "totpEnabledSuccess": "Двухфакторная аутентификация включена", - "totpInvalidCode": "Неверный код, попробуйте снова", - "totpDisableInputRequired": "Введите код TOTP или пароль", - "totpDisabledSuccess": "Двухфакторная аутентификация отключена", - "totpDisableFailed": "Не удалось отключить 2FA", - "totpDisableTitle": "Отключить 2FA", - "totpDisablePlaceholder": "Введите код TOTP или пароль", - "totpDisableConfirm": "Отключить 2FA", - "totpContinueVerify": "Продолжить проверку", - "totpVerifyTitle": "Проверка кода", - "totpBackupTitle": "Резервные коды", - "totpDownloadBackup": "Скачать резервные коды", - "passkeys": "Ключи доступа", - "passkeysDesc": "Используйте учётные данные WebAuthn/FIDO2 для входа без пароля", - "passkeyName": "Название ключа доступа", - "passkeyUvPreferred": "Предпочтительно", - "passkeyUvRequired": "Обязательно", - "passkeyUvDiscouraged": "Не рекомендуется", - "addPasskey": "Добавить ключ доступа", - "noPasskeys": "Нет зарегистрированных ключей доступа.", - "passkeyAdded": "Ключ доступа добавлен", - "passkeyAddFailed": "Не удалось добавить ключ доступа", - "passkeyDeleted": "Ключ доступа удалён", - "passkeyDeleteFailed": "Ошибка удаления ключа доступа", "done": "Готово", - "secretCopied": "Секрет скопирован в буфер обмена", "apiKeyNameRequired": "Требуется имя ключа", "apiKeyCreated": "API-ключ «{{name}}» создан", "apiKeyCreateFailed": "Ошибка создания API-ключа", diff --git a/src/ui/locales/translated/sr_SP.json b/src/ui/locales/translated/sr_SP.json index 49c1d87d9..3292c434a 100644 --- a/src/ui/locales/translated/sr_SP.json +++ b/src/ui/locales/translated/sr_SP.json @@ -1592,8 +1592,6 @@ "hasAccount": "Већ имате налог?", "twoFactorAuth": "Двофакторска аутентификација", "enterCode": "Унесите верификациони код", - "backupCode": "Или користите резервни код", - "verifyCode": "Верификујте код", "redirectingToApp": "Преусмеравање на апликацију...", "sshAuthenticationRequired": "Потребна је SSH аутентификација", "sshNoKeyboardInteractive": "Интерактивна аутентификација преко тастатуре није доступна", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "Аутентификација је онемогућена", "authenticationDisabledDesc": "Све методе аутентификације су тренутно онемогућене. Контактирајте администратора.", "passwordLoginDisabledDesc": "Пријава лозинком је онемогућена. Користите шифру или екстерног добављача аутентификације.", - "signInWithPasskey": "Пријавите се помоћу лозинке", - "passkeyLoginFailed": "Пријава лозинком није успела", "attemptsRemaining": "{{count}} преосталих покушаја", "confirmResetDataWipe": "Овај налог се није пријављивао од надоградње шифровања, тако да се његови сачувани подаци не могу вратити без старе лозинке. Ресетовање ће трајно избрисати његове хостове, акредитиве и фрагменте. Желите да наставите?" }, @@ -2065,7 +2061,6 @@ "saving": "Чување...", "updateRegistrationFailed": "Ажурирање подешавања регистрације није успело", "updatePasswordLoginFailed": "Ажурирање подешавања за пријаву лозинком није успело", - "cannotDisablePasswordLoginWithTotp": "Не можете онемогућити пријаву лозинком док је 2FA омогућена за једног или више корисника. Прво онемогућите 2FA.", "updateOidcAutoProvisionFailed": "Ажурирање подешавања аутоматског обезбеђивања OIDC-а није успело", "updateOidcSilentLoginDefaultFailed": "Није успело ажурирање подешавања тихог пријављивања на OIDC", "updatePasswordResetFailed": "Ажурирање подешавања за ресетовање лозинке није успело", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "Ресетовање лозинке. Шифровани подаци корисника су обрисани.", "resetPasswordFailed": "Ресетовање лозинке није успело", "resetPasswordConfirmWipe": "{{username}} се није пријавио од надоградње шифровања, тако да се његови подаци не могу повратити. Ресетовањем сада ће се избрисати његови хостови, акредитиви и фрагменти. Желите ли да наставите?", - "totpSectionTitle": "Двофакторска аутентификација", - "totpStatusEnabled": "TOTP је омогућен за овог корисника", - "totpStatusDisabled": "TOTP није омогућен за овог корисника", - "disableTotp": "Онемогући", - "disableTotpConfirm": "Онемогућите двофакторску аутентификацију за {{username}}? Моћи ће да се пријаве само са својом лозинком.", - "totpDisabledSuccess": "ТОТП је онемогућен", - "totpDisableFailed": "Онемогућавање TOTP-а није успело", "manageApiKeys": "API кључеви", "apiKeyNamePlaceholder": "Назив кључа", "apiKeyCopyNotice": "Копирајте овај кључ сада, више се неће приказивати.", @@ -2576,23 +2564,6 @@ "settingsUpdates": "Ажурирања", "disableUpdateChecks": "Онемогући провере ажурирања", "disableUpdateChecksDesc": "Престани да провераваш ажурирања", - "totpAuthenticator": "TOTP аутентификатор", - "totpEnabled": "2FA је омогућена", - "totpDisabled": "Додајте додатну безбедност пријављивања", - "totpAddAuthenticator": "Додај уређај", - "totpAddTitle": "Додај уређај за аутентификовање", - "totpAddDescription": "Потврдите свој идентитет да бисте регистровали другог аутентификатора са истим безбедним TOTP кључем.", - "totpAddInputRequired": "Унесите свој TOTP код или лозинку", - "totpAddFailed": "Додавање аутентификатора није успело", - "totpAddScanInstructions": "Скенирајте овај QR код додатним аутентификатором. Постојећи аутентификатори ће наставити да раде.", - "totpAddSuccess": "Додатни аутентификатор је регистрован", - "disable": "Онемогући", - "enable": "Омогући", - "setupTotp": "Подешавање TOTP-а", - "qrCode": "QR код", - "totpInstructions": "Скенирајте QR код или унесите тајни код у апликацију за аутентификацију, а затим унесите шестоцифрени код", - "totpCodePlaceholder": "000000", - "verify": "Верификуј", "changePassword": "Промена лозинке", "currentPasswordLabel": "Тренутна лозинка", "currentPasswordPlaceholder": "Тренутна лозинка", @@ -2638,34 +2609,7 @@ "roleUser": "Корисник", "authMethodDual": "Двострука аутентификација", "authMethodOidc": "ОИДЦ", - "totpSetupFailed": "Покретање подешавања TOTP-а није успело", - "totpEnter6Digits": "Унесите шестоцифрени код", - "totpEnabledSuccess": "Омогућена је двофакторска аутентификација", - "totpInvalidCode": "Неважећи код, покушајте поново", - "totpDisableInputRequired": "Унесите свој TOTP код или лозинку", - "totpDisabledSuccess": "Двофакторска аутентификација је онемогућена", - "totpDisableFailed": "Онемогућавање 2FA није успело", - "totpDisableTitle": "Онемогући 2FA", - "totpDisablePlaceholder": "Унесите TOTP код или лозинку", - "totpDisableConfirm": "Онемогући 2FA", - "totpContinueVerify": "Настави са верификацијом", - "totpVerifyTitle": "Верификујте код", - "totpBackupTitle": "Резервни кодови", - "totpDownloadBackup": "Преузмите резервне кодове", - "passkeys": "Лозинке", - "passkeysDesc": "Користите WebAuthn/FIDO2 акредитиве за пријаву без лозинке", - "passkeyName": "Назив лозинке", - "passkeyUvPreferred": "Преферирано", - "passkeyUvRequired": "Обавезно", - "passkeyUvDiscouraged": "Обесхрабрено", - "addPasskey": "Додај лозинку", - "noPasskeys": "Није регистрована ниједна лозинка.", - "passkeyAdded": "Лозинка је додата", - "passkeyAddFailed": "Додавање лозинке није успело", - "passkeyDeleted": "Лозинка је обрисана", - "passkeyDeleteFailed": "Брисање лозинке није успело", "done": "Готово", - "secretCopied": "Тајни токен је копиран у међуспремник", "apiKeyNameRequired": "Назив кључа је обавезан", "apiKeyCreated": "API кључ „{{name}}“ је креиран", "apiKeyCreateFailed": "Није успело креирање API кључа", diff --git a/src/ui/locales/translated/sv_SE.json b/src/ui/locales/translated/sv_SE.json index 1f008acb3..4679ce011 100644 --- a/src/ui/locales/translated/sv_SE.json +++ b/src/ui/locales/translated/sv_SE.json @@ -1592,8 +1592,6 @@ "hasAccount": "Har du redan ett konto?", "twoFactorAuth": "Tvåfaktorsautentisering", "enterCode": "Ange verifieringskod", - "backupCode": "Eller använd reservkod", - "verifyCode": "Verifiera kod", "redirectingToApp": "Omdirigerar till appen...", "sshAuthenticationRequired": "SSH-autentisering krävs", "sshNoKeyboardInteractive": "Tangentbordsinteraktiv autentisering är inte tillgänglig", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "Autentisering inaktiverad", "authenticationDisabledDesc": "Alla autentiseringsmetoder är för närvarande inaktiverade. Kontakta din administratör.", "passwordLoginDisabledDesc": "Lösenordsinloggning är inaktiverad. Använd en lösenordsnyckel eller en extern autentiseringsleverantör.", - "signInWithPasskey": "Logga in med lösenord", - "passkeyLoginFailed": "Inloggning med lösenordsnyckel misslyckades", "attemptsRemaining": "{{count}} försök återstår", "confirmResetDataWipe": "Det här kontot har inte loggats in sedan krypteringsuppgraderingen, så dess lagrade data kan inte återställas utan det gamla lösenordet. Om du återställer kontot tas dess värdar, inloggningsuppgifter och kodavsnitt permanent bort. Vill du fortsätta?" }, @@ -2065,7 +2061,6 @@ "saving": "Sparande...", "updateRegistrationFailed": "Misslyckades med att uppdatera registreringsinställningen", "updatePasswordLoginFailed": "Misslyckades med att uppdatera lösenordsinställningen för inloggning", - "cannotDisablePasswordLoginWithTotp": "Det går inte att inaktivera lösenordsinloggning medan 2FA är aktiverat för en eller flera användare. Inaktivera 2FA först.", "updateOidcAutoProvisionFailed": "Misslyckades med att uppdatera inställningen för automatisk OIDC-provisionering", "updateOidcSilentLoginDefaultFailed": "Misslyckades med att uppdatera inställningen för tyst OIDC-inloggning", "updatePasswordResetFailed": "Misslyckades med att uppdatera inställningen för lösenordsåterställning", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "Lösenordsåterställning. Användarens krypterade data har raderats.", "resetPasswordFailed": "Misslyckades med att återställa lösenordet", "resetPasswordConfirmWipe": "{{username}} har inte loggat in sedan krypteringsuppgraderingen, så deras data kan inte återställas. Om du återställer nu raderas deras värdar, inloggningsuppgifter och kodavsnitt. Vill du fortsätta?", - "totpSectionTitle": "Tvåfaktorsautentisering", - "totpStatusEnabled": "TOTP är aktiverat för den här användaren", - "totpStatusDisabled": "TOTP är inte aktiverat för den här användaren", - "disableTotp": "Inaktivera", - "disableTotpConfirm": "Inaktivera tvåfaktorsautentisering för {{username}}? De kommer bara att kunna logga in med sitt lösenord.", - "totpDisabledSuccess": "TOTP inaktiverad", - "totpDisableFailed": "Misslyckades med att inaktivera TOTP", "manageApiKeys": "API-nycklar", "apiKeyNamePlaceholder": "Nyckelnamn", "apiKeyCopyNotice": "Kopiera den här nyckeln nu, den kommer inte att visas igen.", @@ -2576,23 +2564,6 @@ "settingsUpdates": "Uppdateringar", "disableUpdateChecks": "Inaktivera uppdateringskontroller", "disableUpdateChecksDesc": "Sluta leta efter uppdateringar", - "totpAuthenticator": "TOTP-autentiserare", - "totpEnabled": "2FA är aktiverat", - "totpDisabled": "Lägg till extra inloggningssäkerhet", - "totpAddAuthenticator": "Lägg till enhet", - "totpAddTitle": "Lägg till autentiseringsenhet", - "totpAddDescription": "Bekräfta din identitet för att registrera en annan autentiserare med samma säkra TOTP-nyckel.", - "totpAddInputRequired": "Ange din TOTP-kod eller ditt lösenord", - "totpAddFailed": "Misslyckades med att lägga till autentiserare", - "totpAddScanInstructions": "Skanna den här QR-koden med den extra autentiseringsfunktionen. Befintliga autentiseringsfunktioner kommer att fortsätta fungera.", - "totpAddSuccess": "Ytterligare autentiserare registrerad", - "disable": "Inaktivera", - "enable": "Aktivera", - "setupTotp": "Konfigurera TOTP", - "qrCode": "QR-kod", - "totpInstructions": "Skanna QR-koden eller ange hemligheten i din autentiseringsapp och ange sedan den 6-siffriga koden", - "totpCodePlaceholder": "000000", - "verify": "Kontrollera", "changePassword": "Ändra lösenord", "currentPasswordLabel": "Nuvarande lösenord", "currentPasswordPlaceholder": "Nuvarande lösenord", @@ -2638,34 +2609,7 @@ "roleUser": "Användare", "authMethodDual": "Dubbel autentisering", "authMethodOidc": "OIDC", - "totpSetupFailed": "Misslyckades med att starta TOTP-installationen", - "totpEnter6Digits": "Ange en 6-siffrig kod", - "totpEnabledSuccess": "Tvåfaktorsautentisering aktiverad", - "totpInvalidCode": "Ogiltig kod, försök igen", - "totpDisableInputRequired": "Ange din TOTP-kod eller ditt lösenord", - "totpDisabledSuccess": "Tvåfaktorsautentisering inaktiverad", - "totpDisableFailed": "Misslyckades med att inaktivera 2FA", - "totpDisableTitle": "Inaktivera 2FA", - "totpDisablePlaceholder": "Ange TOTP-kod eller lösenord", - "totpDisableConfirm": "Inaktivera 2FA", - "totpContinueVerify": "Fortsätt att verifiera", - "totpVerifyTitle": "Verifiera kod", - "totpBackupTitle": "Reservkoder", - "totpDownloadBackup": "Ladda ner reservkoder", - "passkeys": "Lösenord", - "passkeysDesc": "Använd WebAuthn/FIDO2-inloggningsuppgifter för att logga in utan lösenord", - "passkeyName": "Lösenordsnamn", - "passkeyUvPreferred": "Föredragen", - "passkeyUvRequired": "Nödvändig", - "passkeyUvDiscouraged": "Avskräckt", - "addPasskey": "Lägg till lösenord", - "noPasskeys": "Inga lösennycklar registrerade.", - "passkeyAdded": "Lösenord tillagt", - "passkeyAddFailed": "Misslyckades med att lägga till lösenord", - "passkeyDeleted": "Lösenordet raderat", - "passkeyDeleteFailed": "Misslyckades med att ta bort lösenordet", "done": "Gjort", - "secretCopied": "Hemlighet kopierad till urklipp", "apiKeyNameRequired": "Nyckelnamn krävs", "apiKeyCreated": "API-nyckeln \"{{name}}\" skapad", "apiKeyCreateFailed": "Misslyckades med att skapa API-nyckel", diff --git a/src/ui/locales/translated/th_TH.json b/src/ui/locales/translated/th_TH.json index 7db88128b..afc88a9df 100644 --- a/src/ui/locales/translated/th_TH.json +++ b/src/ui/locales/translated/th_TH.json @@ -1592,8 +1592,6 @@ "hasAccount": "มีบัญชีอยู่แล้วใช่ไหม?", "twoFactorAuth": "การตรวจสอบสิทธิ์แบบสองปัจจัย", "enterCode": "ป้อนรหัสยืนยัน", - "backupCode": "หรือใช้รหัสสำรอง", - "verifyCode": "ยืนยันรหัส", "redirectingToApp": "กำลังเปลี่ยนเส้นทางไปยังแอป...", "sshAuthenticationRequired": "ต้องมีการตรวจสอบสิทธิ์ SSH", "sshNoKeyboardInteractive": "การตรวจสอบสิทธิ์แบบโต้ตอบผ่านแป้นพิมพ์ไม่พร้อมใช้งาน", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "การตรวจสอบสิทธิ์ถูกปิดใช้งาน", "authenticationDisabledDesc": "ขณะนี้วิธีการยืนยันตัวตนทั้งหมดถูกปิดใช้งาน โปรดติดต่อผู้ดูแลระบบของคุณ", "passwordLoginDisabledDesc": "การเข้าสู่ระบบด้วยรหัสผ่านถูกปิดใช้งาน โปรดใช้รหัสผ่านเฉพาะหรือผู้ให้บริการตรวจสอบสิทธิ์ภายนอก", - "signInWithPasskey": "ลงชื่อเข้าใช้ด้วยรหัสผ่าน", - "passkeyLoginFailed": "การเข้าสู่ระบบด้วยรหัสผ่านล้มเหลว", "attemptsRemaining": "เหลือการลองอีก {{count}} ครั้ง", "confirmResetDataWipe": "บัญชีนี้ไม่ได้เข้าสู่ระบบตั้งแต่การอัปเกรดการเข้ารหัส ดังนั้นข้อมูลที่จัดเก็บไว้จึงไม่สามารถกู้คืนได้หากไม่มีรหัสผ่านเดิม การรีเซ็ตจะลบโฮสต์ ข้อมูลประจำตัว และส่วนย่อยของบัญชีนี้อย่างถาวร ต้องการดำเนินการต่อหรือไม่?" }, @@ -2065,7 +2061,6 @@ "saving": "ประหยัด...", "updateRegistrationFailed": "ไม่สามารถอัปเดตการตั้งค่าการลงทะเบียนได้", "updatePasswordLoginFailed": "ไม่สามารถอัปเดตการตั้งค่าการเข้าสู่ระบบด้วยรหัสผ่านได้", - "cannotDisablePasswordLoginWithTotp": "ไม่สามารถปิดใช้งานการเข้าสู่ระบบด้วยรหัสผ่านได้ในขณะที่เปิดใช้งาน 2FA สำหรับผู้ใช้อย่างน้อยหนึ่งราย โปรดปิดใช้งาน 2FA ก่อน", "updateOidcAutoProvisionFailed": "ไม่สามารถอัปเดตการตั้งค่าการจัดเตรียมอัตโนมัติของ OIDC ได้", "updateOidcSilentLoginDefaultFailed": "ไม่สามารถอัปเดตการตั้งค่าการเข้าสู่ระบบ OIDC แบบเงียบได้", "updatePasswordResetFailed": "ไม่สามารถอัปเดตการตั้งค่าการรีเซ็ตรหัสผ่านได้", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "รีเซ็ตรหัสผ่าน ข้อมูลที่เข้ารหัสของผู้ใช้ถูกลบออกแล้ว", "resetPasswordFailed": "ไม่สามารถรีเซ็ตรหัสผ่านได้", "resetPasswordConfirmWipe": "ผู้ใช้ {{username}} ไม่ได้ล็อกอินเข้าสู่ระบบตั้งแต่การอัปเกรดการเข้ารหัส ดังนั้นจึงไม่สามารถกู้คืนข้อมูลได้ การรีเซ็ตในขณะนี้จะลบโฮสต์ ข้อมูลประจำตัว และข้อมูลย่อของผู้ใช้ดังกล่าว ต้องการดำเนินการต่อหรือไม่?", - "totpSectionTitle": "การตรวจสอบสิทธิ์แบบสองปัจจัย", - "totpStatusEnabled": "TOTP เปิดใช้งานสำหรับผู้ใช้รายนี้แล้ว", - "totpStatusDisabled": "TOTP ไม่ได้เปิดใช้งานสำหรับผู้ใช้รายนี้", - "disableTotp": "ปิดใช้งาน", - "disableTotpConfirm": "ปิดใช้งานการตรวจสอบสิทธิ์แบบสองขั้นตอนสำหรับ {{username}}หรือไม่? พวกเขาจะสามารถเข้าสู่ระบบได้โดยใช้เพียงรหัสผ่านเท่านั้น", - "totpDisabledSuccess": "TOTP ถูกปิดใช้งาน", - "totpDisableFailed": "ไม่สามารถปิดใช้งาน TOTP ได้", "manageApiKeys": "คีย์ API", "apiKeyNamePlaceholder": "ชื่อหลัก", "apiKeyCopyNotice": "คัดลอกรหัสนี้ไว้ ตอนนี้จะไม่ปรากฏอีกแล้ว", @@ -2576,23 +2564,6 @@ "settingsUpdates": "การอัปเดต", "disableUpdateChecks": "ปิดใช้งานการตรวจสอบการอัปเดต", "disableUpdateChecksDesc": "หยุดตรวจสอบการอัปเดต", - "totpAuthenticator": "ตัวตรวจสอบสิทธิ์ TOTP", - "totpEnabled": "เปิดใช้งาน 2FA แล้ว", - "totpDisabled": "เพิ่มระบบรักษาความปลอดภัยในการเข้าสู่ระบบเพิ่มเติม", - "totpAddAuthenticator": "เพิ่มอุปกรณ์", - "totpAddTitle": "เพิ่มอุปกรณ์ยืนยันตัวตน", - "totpAddDescription": "ยืนยันตัวตนของคุณเพื่อลงทะเบียนแอปตรวจสอบสิทธิ์อีกแอปหนึ่งโดยใช้รหัส TOTP ที่ปลอดภัยเดียวกัน", - "totpAddInputRequired": "ป้อนรหัส TOTP หรือรหัสผ่านของคุณ", - "totpAddFailed": "ไม่สามารถเพิ่มตัวตรวจสอบสิทธิ์ได้", - "totpAddScanInstructions": "สแกนคิวอาร์โค้ดนี้ด้วยอุปกรณ์ยืนยันตัวตนเพิ่มเติม อุปกรณ์ยืนยันตัวตนที่มีอยู่เดิมยังคงใช้งานได้ต่อไป", - "totpAddSuccess": "ลงทะเบียนตัวยืนยันตัวตนเพิ่มเติมแล้ว", - "disable": "ปิดใช้งาน", - "enable": "เปิดใช้งาน", - "setupTotp": "ตั้งค่า TOTP", - "qrCode": "รหัส QR", - "totpInstructions": "สแกนคิวอาร์โค้ดหรือป้อนรหัสลับในแอปยืนยันตัวตนของคุณ จากนั้นป้อนรหัส 6 หลัก", - "totpCodePlaceholder": "000000", - "verify": "ตรวจสอบ", "changePassword": "เปลี่ยนรหัสผ่าน", "currentPasswordLabel": "รหัสผ่านปัจจุบัน", "currentPasswordPlaceholder": "รหัสผ่านปัจจุบัน", @@ -2638,34 +2609,7 @@ "roleUser": "ผู้ใช้", "authMethodDual": "การตรวจสอบสิทธิ์แบบคู่", "authMethodOidc": "โอไอดีซี", - "totpSetupFailed": "ไม่สามารถเริ่มการตั้งค่า TOTP ได้", - "totpEnter6Digits": "ป้อนรหัส 6 หลัก", - "totpEnabledSuccess": "เปิดใช้งานการตรวจสอบสิทธิ์แบบสองขั้นตอน", - "totpInvalidCode": "รหัสไม่ถูกต้อง โปรดลองอีกครั้ง", - "totpDisableInputRequired": "ป้อนรหัส TOTP หรือรหัสผ่านของคุณ", - "totpDisabledSuccess": "ปิดใช้งานการตรวจสอบสิทธิ์แบบสองขั้นตอน", - "totpDisableFailed": "ไม่สามารถปิดใช้งาน 2FA ได้", - "totpDisableTitle": "ปิดใช้งาน 2FA", - "totpDisablePlaceholder": "ป้อนรหัส TOTP หรือรหัสผ่าน", - "totpDisableConfirm": "ปิดใช้งาน 2FA", - "totpContinueVerify": "ดำเนินการตรวจสอบต่อไป", - "totpVerifyTitle": "ยืนยันรหัส", - "totpBackupTitle": "รหัสสำรอง", - "totpDownloadBackup": "ดาวน์โหลดรหัสสำรอง", - "passkeys": "รหัสผ่าน", - "passkeysDesc": "ใช้ข้อมูลรับรอง WebAuthn/FIDO2 เพื่อเข้าสู่ระบบโดยไม่ต้องใช้รหัสผ่าน", - "passkeyName": "ชื่อรหัสผ่าน", - "passkeyUvPreferred": "ที่ต้องการ", - "passkeyUvRequired": "ที่จำเป็น", - "passkeyUvDiscouraged": "ท้อใจ", - "addPasskey": "เพิ่มรหัสผ่าน", - "noPasskeys": "ไม่มีการลงทะเบียนรหัสผ่านใดๆ", - "passkeyAdded": "เพิ่มรหัสผ่านแล้ว", - "passkeyAddFailed": "ไม่สามารถเพิ่มรหัสผ่านได้", - "passkeyDeleted": "รหัสผ่านถูกลบแล้ว", - "passkeyDeleteFailed": "ไม่สามารถลบรหัสผ่านได้", "done": "เสร็จแล้ว", - "secretCopied": "คัดลอกข้อมูลลับไปยังคลิปบอร์ดแล้ว", "apiKeyNameRequired": "จำเป็นต้องระบุชื่อคีย์", "apiKeyCreated": "สร้างคีย์ API \"{{name}}\" แล้ว", "apiKeyCreateFailed": "ไม่สามารถสร้างคีย์ API ได้", diff --git a/src/ui/locales/translated/tr_TR.json b/src/ui/locales/translated/tr_TR.json index 1198e3469..b55618650 100644 --- a/src/ui/locales/translated/tr_TR.json +++ b/src/ui/locales/translated/tr_TR.json @@ -1592,8 +1592,6 @@ "hasAccount": "Zaten hesabınız var mı?", "twoFactorAuth": "İki Faktörlü Kimlik Doğrulama", "enterCode": "Doğrulama kodunu girin", - "backupCode": "Veya yedek kodu kullanın", - "verifyCode": "Kodu Doğrula", "redirectingToApp": "Uygulamaya yönlendiriliyor...", "sshAuthenticationRequired": "SSH Kimlik Doğrulaması Gerekli", "sshNoKeyboardInteractive": "Klavye-Etkileşimli Kimlik Doğrulama Kullanılamıyor", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "Kimlik Doğrulama Devre Dışı", "authenticationDisabledDesc": "Tüm kimlik doğrulama yöntemleri şu anda devre dışı. Lütfen yöneticinizle iletişime geçin.", "passwordLoginDisabledDesc": "Parola ile oturum açma devre dışı. Passkey veya harici bir kimlik doğrulama sağlayıcısı kullanın.", - "signInWithPasskey": "passkey ile oturum aç", - "passkeyLoginFailed": "Passkey girişi başarısız oldu", "attemptsRemaining": "{{count}} deneme kaldı", "confirmResetDataWipe": "Bu hesap, şifreleme güncellemesinden sonra oturum açmadığından, saklanan verileri eski şifre olmadan kurtarılamaz. Sıfırlama, sunucuları, kimlik bilgilerini ve parçacıkları kalıcı olarak siler. Devam edilsin mi?" }, @@ -2065,7 +2061,6 @@ "saving": "Kaydediliyor...", "updateRegistrationFailed": "Kayıt ayarı güncellenemedi", "updatePasswordLoginFailed": "Parola girişi ayarı güncellenemedi", - "cannotDisablePasswordLoginWithTotp": "Bir veya daha fazla kullanıcı için 2FA etkinken parola girişi devre dışı bırakılamaz. Önce 2FA'yı devre dışı bırakın.", "updateOidcAutoProvisionFailed": "OIDC otomatik sağlama ayarı güncellenemedi", "updateOidcSilentLoginDefaultFailed": "Sessiz OIDC girişi ayarı güncellenemedi", "updatePasswordResetFailed": "Parola sıfırlama ayarı güncellenemedi", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "Parola sıfırlandı. Kullanıcının şifrelenmiş verileri silindi.", "resetPasswordFailed": "Parola sıfırlanamadı", "resetPasswordConfirmWipe": "{{username}} şifreleme yükseltmesinden bu yana oturum açmadı, bu nedenle verileri kurtarılamaz. Şimdi sıfırlamak ana makinelerini, kimlik bilgilerini ve kod parçacıklarını siler. Devam edilsin mi?", - "totpSectionTitle": "İki Faktörlü Kimlik Doğrulama", - "totpStatusEnabled": "Bu kullanıcı için TOTP etkin", - "totpStatusDisabled": "Bu kullanıcı için TOTP etkin değil", - "disableTotp": "Devre Dışı Bırak", - "disableTotpConfirm": "{{username}} için iki faktörlü kimlik doğrulama devre dışı bırakılsın mı? Yalnızca parolasıyla oturum açabilecek.", - "totpDisabledSuccess": "TOTP devre dışı bırakıldı", - "totpDisableFailed": "TOTP devre dışı bırakılamadı", "manageApiKeys": "API Anahtarları", "apiKeyNamePlaceholder": "Anahtar adı", "apiKeyCopyNotice": "Bu anahtarı şimdi kopyalayın, bir daha gösterilmeyecektir.", @@ -2576,23 +2564,6 @@ "settingsUpdates": "Güncellemeler", "disableUpdateChecks": "Güncelleme Denetimlerini Devre Dışı Bırak", "disableUpdateChecksDesc": "Güncellemeleri denetlemeyi durdurun", - "totpAuthenticator": "TOTP Kimlik Doğrulayıcı", - "totpEnabled": "2FA etkin", - "totpDisabled": "Ek giriş güvenliği ekleyin", - "totpAddAuthenticator": "Cihaz ekle", - "totpAddTitle": "Kimlik doğrulama cihazı ekle", - "totpAddDescription": "Aynı güvenli TOTP anahtarıyla başka bir kimlik doğrulayıcıyı kaydetmek için kimliğinizi doğrulayın.", - "totpAddInputRequired": "TOTP kodunuzu veya şifrenizi girin.", - "totpAddFailed": "Kimlik doğrulayıcı eklenemedi.", - "totpAddScanInstructions": "Bu QR kodunu ek kimlik doğrulayıcı ile tarayın. Mevcut kimlik doğrulayıcılar çalışmaya devam edecektir.", - "totpAddSuccess": "Ek kimlik doğrulayıcı kaydedildi.", - "disable": "Devre Dışı Bırak", - "enable": "Etkinleştir", - "setupTotp": "TOTP Kurulumu", - "qrCode": "QR Kodu", - "totpInstructions": "QR kodunu tarayın veya kimlik doğrulama uygulamanıza gizli anahtarı girin, ardından 6 haneli kodu girin", - "totpCodePlaceholder": "000000", - "verify": "Doğrula", "changePassword": "Şifre Değiştir", "currentPasswordLabel": "Mevcut Şifre", "currentPasswordPlaceholder": "Mevcut şifre", @@ -2638,34 +2609,7 @@ "roleUser": "Kullanıcı", "authMethodDual": "Çift Kimlik Doğrulama", "authMethodOidc": "OIDC", - "totpSetupFailed": "TOTP kurulumu başlatılamadı", - "totpEnter6Digits": "6 haneli bir kod girin", - "totpEnabledSuccess": "İki faktörlü kimlik doğrulama etkinleştirildi", - "totpInvalidCode": "Geçersiz kod, lütfen tekrar deneyin", - "totpDisableInputRequired": "TOTP kodunuzu veya parolanızı girin", - "totpDisabledSuccess": "İki faktörlü kimlik doğrulama devre dışı bırakıldı", - "totpDisableFailed": "2FA devre dışı bırakılamadı", - "totpDisableTitle": "2FA'yı Devre Dışı Bırak", - "totpDisablePlaceholder": "TOTP kodu veya parola girin", - "totpDisableConfirm": "2FA'yı Devre Dışı Bırak", - "totpContinueVerify": "Doğrulamaya Devam Et", - "totpVerifyTitle": "Kodu Doğrula", - "totpBackupTitle": "Yedek Kodlar", - "totpDownloadBackup": "Yedek Kodları İndir", - "passkeys": "Geçiş Anahtarları", - "passkeysDesc": "Parola olmadan oturum açmak için WebAuthn/FIDO2 kimlik bilgilerini kullanın", - "passkeyName": "Geçiş anahtarı adı", - "passkeyUvPreferred": "Tercih edilen", - "passkeyUvRequired": "Gerekli", - "passkeyUvDiscouraged": "Önerilmeyen", - "addPasskey": "Geçiş Anahtarı Ekle", - "noPasskeys": "Kayıtlı geçiş anahtarı yok.", - "passkeyAdded": "Geçiş anahtarı eklendi", - "passkeyAddFailed": "Geçiş anahtarı eklenemedi", - "passkeyDeleted": "Geçiş anahtarı silindi", - "passkeyDeleteFailed": "Geçiş anahtarı silinemedi", "done": "Tamam", - "secretCopied": "Gizli anahtar panoya kopyalandı", "apiKeyNameRequired": "Anahtar adı zorunludur", "apiKeyCreated": "API anahtarı \"{{name}}\" oluşturuldu", "apiKeyCreateFailed": "API anahtarı oluşturulamadı", diff --git a/src/ui/locales/translated/uk_UA.json b/src/ui/locales/translated/uk_UA.json index d0c976f41..c2c1e3fa1 100644 --- a/src/ui/locales/translated/uk_UA.json +++ b/src/ui/locales/translated/uk_UA.json @@ -1592,8 +1592,6 @@ "hasAccount": "Вже маєте обліковий запис?", "twoFactorAuth": "Двофакторна автентифікація", "enterCode": "Введіть код підтвердження", - "backupCode": "Або скористайтеся резервним кодом", - "verifyCode": "Підтвердити код", "redirectingToApp": "Перенаправлення до програми...", "sshAuthenticationRequired": "Потрібна автентифікація SSH", "sshNoKeyboardInteractive": "Автентифікація з клавіатури недоступна", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "Автентифікацію вимкнено", "authenticationDisabledDesc": "Усі методи автентифікації наразі вимкнено. Зверніться до свого адміністратора.", "passwordLoginDisabledDesc": "Вхід за паролем вимкнено. Використайте ключ доступу або зовнішній постачальник автентифікації.", - "signInWithPasskey": "Увійти за допомогою ключа доступу", - "passkeyLoginFailed": "Не вдалося ввійти з ключем доступу", "attemptsRemaining": "Залишилося {{count}} спроб", "confirmResetDataWipe": "Цей обліковий запис не входив у систему з моменту оновлення шифрування, тому його збережені дані неможливо відновити без старого пароля. Скидання налаштувань призведе до остаточного видалення його хостів, облікових даних та фрагментів. Продовжити?" }, @@ -2065,7 +2061,6 @@ "saving": "Збереження...", "updateRegistrationFailed": "Не вдалося оновити налаштування реєстрації", "updatePasswordLoginFailed": "Не вдалося оновити налаштування входу з паролем", - "cannotDisablePasswordLoginWithTotp": "Неможливо вимкнути вхід за паролем, якщо для одного або кількох користувачів увімкнено двофакторну аутентифікацію. Спочатку вимкніть двофакторну аутентифікацію.", "updateOidcAutoProvisionFailed": "Не вдалося оновити налаштування автоматичного налаштування OIDC", "updateOidcSilentLoginDefaultFailed": "Не вдалося оновити налаштування тихого входу OIDC", "updatePasswordResetFailed": "Не вдалося оновити налаштування скидання пароля", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "Скидання пароля. Зашифровані дані користувача було видалено.", "resetPasswordFailed": "Не вдалося скинути пароль", "resetPasswordConfirmWipe": "{{username}} не входив у систему після оновлення шифрування, тому його дані неможливо відновити. Скидання налаштувань зараз призведе до видалення його хостів, облікових даних та фрагментів. Продовжити?", - "totpSectionTitle": "Двофакторна автентифікація", - "totpStatusEnabled": "TOTP увімкнено для цього користувача", - "totpStatusDisabled": "TOTP не ввімкнено для цього користувача", - "disableTotp": "Вимкнути", - "disableTotpConfirm": "Вимкнути двофакторну автентифікацію для {{username}}? Вони зможуть увійти лише за допомогою пароля.", - "totpDisabledSuccess": "TOTP вимкнено", - "totpDisableFailed": "Не вдалося вимкнути TOTP", "manageApiKeys": "Ключі API", "apiKeyNamePlaceholder": "Назва ключа", "apiKeyCopyNotice": "Скопіюйте цей ключ зараз, він більше не відображатиметься.", @@ -2576,23 +2564,6 @@ "settingsUpdates": "Оновлення", "disableUpdateChecks": "Вимкнути перевірки оновлень", "disableUpdateChecksDesc": "Припинити перевірку оновлень", - "totpAuthenticator": "Аутентифікатор TOTP", - "totpEnabled": "Двофакторна аутентифікація (2FA) увімкнена", - "totpDisabled": "Додайте додаткову безпеку входу", - "totpAddAuthenticator": "Додати пристрій", - "totpAddTitle": "Додати пристрій автентифікатора", - "totpAddDescription": "Підтвердьте свою особу, щоб зареєструвати інший автентифікатор з тим самим безпечним ключем TOTP.", - "totpAddInputRequired": "Введіть свій TOTP-код або пароль", - "totpAddFailed": "Не вдалося додати автентифікатор", - "totpAddScanInstructions": "Відскануйте цей QR-код за допомогою додаткового автентифікатора. Існуючі автентифікатори продовжуватимуть працювати.", - "totpAddSuccess": "Зареєстровано додатковий автентифікатор", - "disable": "Вимкнути", - "enable": "Увімкнути", - "setupTotp": "Налаштування TOTP", - "qrCode": "QR-код", - "totpInstructions": "Відскануйте QR-код або введіть секретний код у своєму додатку для автентифікації, а потім введіть 6-значний код", - "totpCodePlaceholder": "000000", - "verify": "Перевірити", "changePassword": "Змінити пароль", "currentPasswordLabel": "Поточний пароль", "currentPasswordPlaceholder": "Поточний пароль", @@ -2638,34 +2609,7 @@ "roleUser": "Користувач", "authMethodDual": "Подвійна автентифікація", "authMethodOidc": "ОІДК", - "totpSetupFailed": "Не вдалося розпочати налаштування TOTP", - "totpEnter6Digits": "Введіть 6-значний код", - "totpEnabledSuccess": "Двофакторна автентифікація ввімкнена", - "totpInvalidCode": "Недійсний код, спробуйте ще раз", - "totpDisableInputRequired": "Введіть свій TOTP-код або пароль", - "totpDisabledSuccess": "Двофакторну автентифікацію вимкнено", - "totpDisableFailed": "Не вдалося вимкнути 2FA", - "totpDisableTitle": "Вимкнути 2FA", - "totpDisablePlaceholder": "Введіть код TOTP або пароль", - "totpDisableConfirm": "Вимкнути 2FA", - "totpContinueVerify": "Продовжити перевірку", - "totpVerifyTitle": "Підтвердити код", - "totpBackupTitle": "Резервні коди", - "totpDownloadBackup": "Завантажити резервні коди", - "passkeys": "Ключі доступу", - "passkeysDesc": "Використовуйте облікові дані WebAuthn/FIDO2 для входу без пароля", - "passkeyName": "Ім'я ключа доступу", - "passkeyUvPreferred": "Бажаний", - "passkeyUvRequired": "Обов'язково", - "passkeyUvDiscouraged": "Знеохочений", - "addPasskey": "Додати пароль", - "noPasskeys": "Не зареєстровано ключів доступу.", - "passkeyAdded": "Ключ доступу додано", - "passkeyAddFailed": "Не вдалося додати ключ доступу", - "passkeyDeleted": "Ключ доступу видалено", - "passkeyDeleteFailed": "Не вдалося видалити ключ доступу", "done": "Готово", - "secretCopied": "Секрет скопійовано в буфер обміну", "apiKeyNameRequired": "Потрібне ім'я ключа", "apiKeyCreated": "Ключ API \"{{name}}\" створено", "apiKeyCreateFailed": "Не вдалося створити ключ API", diff --git a/src/ui/locales/translated/vi_VN.json b/src/ui/locales/translated/vi_VN.json index 74ca8def1..becffbe9e 100644 --- a/src/ui/locales/translated/vi_VN.json +++ b/src/ui/locales/translated/vi_VN.json @@ -1592,8 +1592,6 @@ "hasAccount": "Đã có tài khoản?", "twoFactorAuth": "Xác thực hai yếu tố", "enterCode": "Nhập mã xác minh", - "backupCode": "Hoặc dùng mã dự phòng", - "verifyCode": "Xác minh mã", "redirectingToApp": "Đang chuyển hướng đến ứng dụng...", "sshAuthenticationRequired": "Yêu cầu xác thực SSH", "sshNoKeyboardInteractive": "Xác thực tương tác bàn phím không khả dụng", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "Xác thực bị vô hiệu", "authenticationDisabledDesc": "Tất cả các phương thức xác thực đang bị vô hiệu. Vui lòng liên hệ quản trị viên của bạn.", "passwordLoginDisabledDesc": "Đăng nhập bằng mật khẩu bị vô hiệu. Hãy sử dụng khóa truy cập hoặc nhà cung cấp xác thực bên ngoài.", - "signInWithPasskey": "Đăng nhập bằng khóa truy cập", - "passkeyLoginFailed": "Đăng nhập bằng passkey thất bại", "attemptsRemaining": "Còn {{count}} lần thử", "confirmResetDataWipe": "Tài khoản này chưa đăng nhập kể từ khi nâng cấp mã hóa, do đó dữ liệu đã lưu không thể khôi phục nếu không có mật khẩu cũ. Việc đặt lại sẽ xóa vĩnh viễn các máy chủ, thông tin xác thực và đoạn mã. Tiếp tục?" }, @@ -2065,7 +2061,6 @@ "saving": "Đang lưu...", "updateRegistrationFailed": "Cập nhật cài đặt đăng ký thất bại", "updatePasswordLoginFailed": "Cập nhật cài đặt đăng nhập bằng mật khẩu thất bại", - "cannotDisablePasswordLoginWithTotp": "Không thể vô hiệu hóa đăng nhập bằng mật khẩu khi 2FA đang được bật cho một hoặc nhiều người dùng. Hãy tắt 2FA trước.", "updateOidcAutoProvisionFailed": "Cập nhật cài đặt tự động cấp quyền OIDC thất bại", "updateOidcSilentLoginDefaultFailed": "Cập nhật cài đặt đăng nhập OIDC im lặng thất bại", "updatePasswordResetFailed": "Cập nhật cài đặt đặt lại mật khẩu thất bại", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "Đã đặt lại mật khẩu. Dữ liệu mã hóa của người dùng đã bị xóa.", "resetPasswordFailed": "Không thể đặt lại mật khẩu", "resetPasswordConfirmWipe": "{{username}} chưa đăng nhập kể từ khi nâng cấp mã hóa, do đó dữ liệu của họ không thể khôi phục. Đặt lại ngay bây giờ sẽ xóa máy chủ, thông tin đăng nhập và đoạn mã của họ. Tiếp tục?", - "totpSectionTitle": "Xác thực hai yếu tố", - "totpStatusEnabled": "TOTP đã được bật cho người dùng này", - "totpStatusDisabled": "TOTP chưa được bật cho người dùng này", - "disableTotp": "Vô hiệu hóa", - "disableTotpConfirm": "Vô hiệu hóa xác thực hai yếu tố cho {{username}}? Họ sẽ có thể đăng nhập chỉ bằng mật khẩu.", - "totpDisabledSuccess": "Đã vô hiệu hóa TOTP", - "totpDisableFailed": "Không thể vô hiệu hóa TOTP", "manageApiKeys": "Khóa API", "apiKeyNamePlaceholder": "Tên khóa", "apiKeyCopyNotice": "Sao chép khóa này ngay, sẽ không hiển thị lại.", @@ -2576,23 +2564,6 @@ "settingsUpdates": "Cập nhật", "disableUpdateChecks": "Tắt kiểm tra cập nhật", "disableUpdateChecksDesc": "Ngừng kiểm tra cập nhật", - "totpAuthenticator": "Trình xác thực TOTP", - "totpEnabled": "2FA đã được bật", - "totpDisabled": "Thêm bảo mật đăng nhập bổ sung", - "totpAddAuthenticator": "Thêm thiết bị", - "totpAddTitle": "Thêm thiết bị xác thực", - "totpAddDescription": "Xác nhận danh tính của bạn để đăng ký một thiết bị xác thực khác có cùng khóa TOTP bảo mật.", - "totpAddInputRequired": "Nhập mã TOTP hoặc mật khẩu của bạn", - "totpAddFailed": "Thêm trình xác thực không thành công", - "totpAddScanInstructions": "Quét mã QR này bằng thiết bị xác thực bổ sung. Các thiết bị xác thực hiện có vẫn sẽ hoạt động bình thường.", - "totpAddSuccess": "Đã đăng ký thêm trình xác thực.", - "disable": "Tắt", - "enable": "Bật", - "setupTotp": "Thiết lập TOTP", - "qrCode": "Mã QR", - "totpInstructions": "Quét mã QR hoặc nhập mã bí mật vào ứng dụng xác thực, sau đó nhập mã 6 chữ số", - "totpCodePlaceholder": "000000", - "verify": "Xác minh", "changePassword": "Đổi mật khẩu", "currentPasswordLabel": "Mật khẩu hiện tại", "currentPasswordPlaceholder": "Mật khẩu hiện tại", @@ -2638,34 +2609,7 @@ "roleUser": "Người dùng", "authMethodDual": "Xác thực kép", "authMethodOidc": "OIDC", - "totpSetupFailed": "Không khởi động được cài đặt TOTP", - "totpEnter6Digits": "Nhập mã 6 chữ số", - "totpEnabledSuccess": "Đã bật xác thực hai yếu tố", - "totpInvalidCode": "Mã không hợp lệ, vui lòng thử lại", - "totpDisableInputRequired": "Nhập mã TOTP hoặc mật khẩu của bạn", - "totpDisabledSuccess": "Đã tắt xác thực hai yếu tố", - "totpDisableFailed": "Không tắt được 2FA", - "totpDisableTitle": "Tắt 2FA", - "totpDisablePlaceholder": "Nhập mã TOTP hoặc mật khẩu", - "totpDisableConfirm": "Tắt 2FA", - "totpContinueVerify": "Tiếp tục xác minh", - "totpVerifyTitle": "Xác minh mã", - "totpBackupTitle": "Mã dự phòng", - "totpDownloadBackup": "Tải mã dự phòng", - "passkeys": "Passkeys", - "passkeysDesc": "Sử dụng thông tin đăng nhập WebAuthn/FIDO2 để đăng nhập không cần mật khẩu", - "passkeyName": "Tên passkey", - "passkeyUvPreferred": "Ưu tiên", - "passkeyUvRequired": "Bắt buộc", - "passkeyUvDiscouraged": "Không khuyến khích", - "addPasskey": "Thêm Passkey", - "noPasskeys": "Chưa đăng ký passkey nào.", - "passkeyAdded": "Đã thêm Passkey", - "passkeyAddFailed": "Không thêm được Passkey", - "passkeyDeleted": "Đã xóa Passkey", - "passkeyDeleteFailed": "Xóa passkey thất bại", "done": "Xong", - "secretCopied": "Đã sao chép mã bí mật vào clipboard", "apiKeyNameRequired": "Tên khóa là bắt buộc", "apiKeyCreated": "Đã tạo khóa API \"{{name}}\"", "apiKeyCreateFailed": "Tạo khóa API thất bại", diff --git a/src/ui/locales/translated/zh_CN.json b/src/ui/locales/translated/zh_CN.json index bb3a7bfa6..6aa0e0505 100644 --- a/src/ui/locales/translated/zh_CN.json +++ b/src/ui/locales/translated/zh_CN.json @@ -1592,8 +1592,6 @@ "hasAccount": "已有账号?", "twoFactorAuth": "双因素认证", "enterCode": "输入验证码", - "backupCode": "或使用备用验证码", - "verifyCode": "验证", "redirectingToApp": "正在重定向到应用...", "sshAuthenticationRequired": "需要SSH身份验证", "sshNoKeyboardInteractive": "键盘交互式验证不可用", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "身份验证已禁用", "authenticationDisabledDesc": "当前所有身份验证方法均已禁用。请联系管理员。", "passwordLoginDisabledDesc": "密码登录已禁用。请使用通行密钥或外部身份验证提供程序。", - "signInWithPasskey": "使用通行密钥登录", - "passkeyLoginFailed": "通行密钥登录失败", "attemptsRemaining": "剩余 {{count}} 次尝试", "confirmResetDataWipe": "自加密升级以来,此帐户未登录,因此没有旧密码无法恢复其存储的数据。重置将永久删除其主机、凭据和代码片段。是否继续?" }, @@ -2065,7 +2061,6 @@ "saving": "保存中...", "updateRegistrationFailed": "更新注册设置失败", "updatePasswordLoginFailed": "更新密码登录设置失败", - "cannotDisablePasswordLoginWithTotp": "一个或多个用户启用了 2FA 时不能禁用密码登录。请先禁用 2FA。", "updateOidcAutoProvisionFailed": "更新 OIDC 自动配置设置失败", "updateOidcSilentLoginDefaultFailed": "更新静默 OIDC 登录设置失败", "updatePasswordResetFailed": "更新密码重置设置失败", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "密码已重置。用户的加密数据已被擦除。", "resetPasswordFailed": "密码重置失败", "resetPasswordConfirmWipe": "{{username}} 自加密升级后尚未登录,因此其数据无法恢复。现在重置将删除其主机、凭据和代码片段。是否继续?", - "totpSectionTitle": "双因素认证", - "totpStatusEnabled": "此用户已启用 TOTP", - "totpStatusDisabled": "此用户未启用 TOTP", - "disableTotp": "禁用", - "disableTotpConfirm": "为 {{username}} 禁用双因素认证?他们将只能使用密码登录。", - "totpDisabledSuccess": "TOTP 已禁用", - "totpDisableFailed": "TOTP 禁用失败", "manageApiKeys": "API 密钥", "apiKeyNamePlaceholder": "密钥名称", "apiKeyCopyNotice": "立即复制此密钥,之后将不再显示。", @@ -2576,23 +2564,6 @@ "settingsUpdates": "更新", "disableUpdateChecks": "禁用更新检查", "disableUpdateChecksDesc": "停止检查更新", - "totpAuthenticator": "TOTP 身份验证器", - "totpEnabled": "双因素认证已启用", - "totpDisabled": "添加额外的登录安全", - "totpAddAuthenticator": "添加设备", - "totpAddTitle": "添加身份验证器设备", - "totpAddDescription": "请确认您的身份,以便使用相同的安全 TOTP 密钥注册另一个身份验证器。", - "totpAddInputRequired": "请输入您的 TOTP 验证码或密码", - "totpAddFailed": "添加身份验证器失败", - "totpAddScanInstructions": "请使用新增的验证器扫描此二维码。现有验证器将继续有效。", - "totpAddSuccess": "已注册其他身份验证器", - "disable": "禁用", - "enable": "启用", - "setupTotp": "设置 TOTP", - "qrCode": "二维码", - "totpInstructions": "扫描二维码或在您的身份验证器应用中输入密钥,然后输入6位数字代码", - "totpCodePlaceholder": "000000", - "verify": "验证", "changePassword": "修改密码", "currentPasswordLabel": "当前密码", "currentPasswordPlaceholder": "当前密码", @@ -2638,34 +2609,7 @@ "roleUser": "用户", "authMethodDual": "双重认证", "authMethodOidc": "OIDC", - "totpSetupFailed": "启动 TOTP 设置失败", - "totpEnter6Digits": "输入 6 位验证码", - "totpEnabledSuccess": "双因素认证已启用", - "totpInvalidCode": "验证码无效,请重试", - "totpDisableInputRequired": "输入您的 TOTP 验证码或密码", - "totpDisabledSuccess": "双因素认证已禁用", - "totpDisableFailed": "禁用 2FA 失败", - "totpDisableTitle": "禁用 2FA", - "totpDisablePlaceholder": "输入 TOTP 验证码或密码", - "totpDisableConfirm": "禁用 2FA", - "totpContinueVerify": "继续验证", - "totpVerifyTitle": "验证验证码", - "totpBackupTitle": "备份验证码", - "totpDownloadBackup": "下载备份验证码", - "passkeys": "通行密钥", - "passkeysDesc": "使用 WebAuthn/FIDO2 凭据进行无密码登录", - "passkeyName": "通行密钥名称", - "passkeyUvPreferred": "首选", - "passkeyUvRequired": "必需", - "passkeyUvDiscouraged": "不鼓励", - "addPasskey": "添加通行密钥", - "noPasskeys": "未注册通行密钥。", - "passkeyAdded": "通行密钥已添加", - "passkeyAddFailed": "添加通行密钥失败", - "passkeyDeleted": "通行密钥已删除", - "passkeyDeleteFailed": "删除通行密钥失败", "done": "完成", - "secretCopied": "密钥已复制到剪贴板", "apiKeyNameRequired": "密钥名称为必填项", "apiKeyCreated": "API 密钥 \"{{name}}\" 已创建", "apiKeyCreateFailed": "创建 API 密钥失败", diff --git a/src/ui/locales/translated/zh_TW.json b/src/ui/locales/translated/zh_TW.json index 8f409cbf5..9b6d4cd38 100644 --- a/src/ui/locales/translated/zh_TW.json +++ b/src/ui/locales/translated/zh_TW.json @@ -1592,8 +1592,6 @@ "hasAccount": "已經有帳號了嗎?", "twoFactorAuth": "雙重驗證", "enterCode": "輸入驗證碼", - "backupCode": "或使用備用驗證碼", - "verifyCode": "驗證", "redirectingToApp": "正在重新導向至應用程式...", "sshAuthenticationRequired": "需要 SSH 驗證", "sshNoKeyboardInteractive": "無法使用鍵盤互動式驗證", @@ -1637,8 +1635,6 @@ "authenticationDisabled": "驗證已停用", "authenticationDisabledDesc": "目前所有驗證方法均已停用。請聯絡您的管理員。", "passwordLoginDisabledDesc": "密碼登入已停用。請使用通行密鑰或外部驗證提供者。", - "signInWithPasskey": "使用通行密鑰登入", - "passkeyLoginFailed": "Passkey 登入失敗", "attemptsRemaining": "剩餘 {{count}} 次嘗試", "confirmResetDataWipe": "此帳戶自加密升級後尚未登入,因此若無舊密碼,其儲存的資料將無法復原。重設將會永久刪除其主機、憑證與程式碼片段。是否繼續?" }, @@ -2065,7 +2061,6 @@ "saving": "儲存中...", "updateRegistrationFailed": "更新註冊設定失敗", "updatePasswordLoginFailed": "更新密碼登入設定失敗", - "cannotDisablePasswordLoginWithTotp": "當一或多個使用者啟用雙重驗證 (2FA) 時,無法停用密碼登入。請先停用 2FA。", "updateOidcAutoProvisionFailed": "更新 OIDC 自動佈建設定失敗", "updateOidcSilentLoginDefaultFailed": "更新無提示 OIDC 登入設定失敗", "updatePasswordResetFailed": "更新密碼重設設定失敗", @@ -2112,13 +2107,6 @@ "resetPasswordSuccessWiped": "密碼已重設。使用者的加密資料已清除。", "resetPasswordFailed": "無法重設密碼", "resetPasswordConfirmWipe": "{{username}} 自加密升級後尚未登入,因此其資料無法復原。現在重設將刪除其主機、憑證和程式碼片段。要繼續嗎?", - "totpSectionTitle": "雙因素驗證", - "totpStatusEnabled": "此使用者已啟用 TOTP", - "totpStatusDisabled": "此使用者未啟用 TOTP", - "disableTotp": "停用", - "disableTotpConfirm": "確定要停用 {{username}} 的雙因素驗證嗎?停用後該使用者將只能使用密碼登入。", - "totpDisabledSuccess": "TOTP 已停用", - "totpDisableFailed": "無法停用 TOTP", "manageApiKeys": "API 金鑰", "apiKeyNamePlaceholder": "金鑰名稱", "apiKeyCopyNotice": "立即複製此金鑰,之後將不會再顯示。", @@ -2576,23 +2564,6 @@ "settingsUpdates": "更新", "disableUpdateChecks": "停用更新檢查", "disableUpdateChecksDesc": "停止檢查更新", - "totpAuthenticator": "TOTP 驗證器", - "totpEnabled": "雙因素驗證已啟用", - "totpDisabled": "新增額外的登入安全性", - "totpAddAuthenticator": "新增設備", - "totpAddTitle": "新增身份驗證器設備", - "totpAddDescription": "請確認您的身份,以便使用相同的安全 TOTP 金鑰註冊另一個身份驗證器。", - "totpAddInputRequired": "請輸入您的 TOTP 驗證碼或密碼", - "totpAddFailed": "新增身份驗證器失敗", - "totpAddScanInstructions": "請使用新增的驗證器掃描此二維碼。現有驗證器將繼續有效。", - "totpAddSuccess": "已註冊其他身份驗證器", - "disable": "停用", - "enable": "啟用", - "setupTotp": "設定 TOTP", - "qrCode": "QR 碼", - "totpInstructions": "掃描 QR 碼或在您的驗證器應用程式中輸入金鑰,然後輸入 6 位數驗證碼", - "totpCodePlaceholder": "000000", - "verify": "驗證", "changePassword": "變更密碼", "currentPasswordLabel": "目前密碼", "currentPasswordPlaceholder": "目前密碼", @@ -2638,34 +2609,7 @@ "roleUser": "使用者", "authMethodDual": "雙重驗證", "authMethodOidc": "OIDC", - "totpSetupFailed": "無法啟動 TOTP 設定", - "totpEnter6Digits": "輸入 6 位數驗證碼", - "totpEnabledSuccess": "雙因素驗證已啟用", - "totpInvalidCode": "驗證碼無效,請再試一次", - "totpDisableInputRequired": "輸入您的 TOTP 驗證碼或密碼", - "totpDisabledSuccess": "雙因素驗證已停用", - "totpDisableFailed": "無法停用雙因素驗證", - "totpDisableTitle": "停用雙因素驗證", - "totpDisablePlaceholder": "輸入 TOTP 驗證碼或密碼", - "totpDisableConfirm": "停用雙因素驗證", - "totpContinueVerify": "繼續驗證", - "totpVerifyTitle": "驗證碼", - "totpBackupTitle": "備用驗證碼", - "totpDownloadBackup": "下載備用驗證碼", - "passkeys": "通行金鑰", - "passkeysDesc": "使用 WebAuthn/FIDO2 憑證無需密碼即可登入", - "passkeyName": "通行金鑰名稱", - "passkeyUvPreferred": "偏好", - "passkeyUvRequired": "需要", - "passkeyUvDiscouraged": "不鼓勵", - "addPasskey": "新增通行金鑰", - "noPasskeys": "尚未註冊通行金鑰。", - "passkeyAdded": "通行金鑰已新增", - "passkeyAddFailed": "無法新增通行金鑰", - "passkeyDeleted": "通行金鑰已刪除", - "passkeyDeleteFailed": "刪除通行密鑰失敗", "done": "完成", - "secretCopied": "密鑰已複製到剪貼簿", "apiKeyNameRequired": "必須提供金鑰名稱", "apiKeyCreated": "API 金鑰「{{name}}」已建立", "apiKeyCreateFailed": "建立 API 金鑰失敗", diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts index 9f696f061..a96686ee4 100644 --- a/src/ui/main-axios.ts +++ b/src/ui/main-axios.ts @@ -87,7 +87,10 @@ export interface AuthResponse { } export interface UserInfo { + /** Any second factor, kept under its 2.8 name for older clients. */ totp_enabled: boolean; + /** Admin user list only. */ + second_factor_enabled?: boolean; userId: string; username: string; is_admin: boolean; @@ -1759,7 +1762,6 @@ export { updateOIDCConfig, disableOIDCConfig, adminResetUserPassword, - adminDisableUserTotp, adminExportUserData, type ApiKey, type CreatedApiKey, @@ -1786,11 +1788,6 @@ export { } from "@/api/admin-user-data-api"; export { - setupTOTP, - enableTOTP, - disableTOTP, - verifyTOTPLogin, - generateBackupCodes, getUserAlerts, dismissAlert, getReleasesRSS, diff --git a/src/ui/plugin-host/app.ts b/src/ui/plugin-host/app.ts index f9f1a19e6..a499e26c1 100644 --- a/src/ui/plugin-host/app.ts +++ b/src/ui/plugin-host/app.ts @@ -350,6 +350,9 @@ export function createPluginApp( titleKey: key(method.titleKey), pluginId, component: scoped(method.component) as never, + enrollment: method.enrollment + ? (scoped(method.enrollment) as never) + : undefined, }), ); }, diff --git a/src/ui/plugin-host/auth-registry.ts b/src/ui/plugin-host/auth-registry.ts index 7c83a445d..558c2e29d 100644 --- a/src/ui/plugin-host/auth-registry.ts +++ b/src/ui/plugin-host/auth-registry.ts @@ -11,9 +11,9 @@ import { createRegistry } from "@/lib/registry"; * SSH auth editors render in the host and credential editors when their auth * type is picked. Login methods render on the login screen for methods the * server reports as enabled; second factors render in the step after a first - * login and, when they bring one, in Settings > Security for enrolment. - * Core's own (OIDC, LDAP, passkeys, TOTP) register from - * src/ui/auth/legacy-auth-ui.tsx until they move into plugins. + * login. Either kind may bring an enrolment section for Settings > Security. + * Core's own (OIDC, LDAP) register from src/ui/auth/legacy-auth-ui.tsx until + * they move into plugins. */ export interface SshAuthEditorDef { /** The authType value stored on the host. */ @@ -34,6 +34,9 @@ export interface LoginMethodDef { titleKey: string; icon?: ComponentType<{ className?: string }>; component: ComponentType; + /** Also drawn under the password form. */ + placement?: "inline"; + enrollment?: ComponentType>; } export interface SecondFactorDef { @@ -55,10 +58,12 @@ export const useSshAuthEditors = sshAuthEditors.useList; export const registerLoginMethod = loginMethods.register; export const getLoginMethodUI = loginMethods.get; export const useLoginMethods = loginMethods.useList; +export const listLoginMethodUIs = loginMethods.list; export const registerSecondFactor = secondFactors.register; export const getSecondFactorUI = secondFactors.get; export const useSecondFactors = secondFactors.useList; +export const listSecondFactorUIs = secondFactors.list; export function resetAuthRegistries(): void { sshAuthEditors.reset(); diff --git a/src/ui/plugin-host/testing-host.tsx b/src/ui/plugin-host/testing-host.tsx index 97b0c24ff..ba337b709 100644 --- a/src/ui/plugin-host/testing-host.tsx +++ b/src/ui/plugin-host/testing-host.tsx @@ -42,6 +42,12 @@ import { listSettingsComponents, } from "@/settings/settings-components"; import { getSlotContributions, listActions } from "@/shell/action-registry"; +import { + getLoginMethodUI, + getSecondFactorUI, + listLoginMethodUIs, + listSecondFactorUIs, +} from "./auth-registry"; import { ActionSlot, ComponentSlot } from "@/shell/ActionSlot"; import type { Host, Tab } from "@/types/ui-types"; import { createPluginApp } from "./app"; @@ -223,6 +229,9 @@ export async function renderPlugin( (contribution) => contribution.actionId, ), actions: () => mine(listActions()).map((action) => action.id), + loginMethods: () => mine(listLoginMethodUIs()).map((method) => method.id), + secondFactors: () => + mine(listSecondFactorUIs()).map((factor) => factor.id), }, renderTab(type, props = {}) { @@ -315,6 +324,46 @@ export async function renderPlugin( ); }, + renderLoginMethod(id, props = {}) { + const method = getLoginMethodUI(id) ?? missing("a login method", id); + const Component = method.component; + return wrap( + {}} + startRedirect={async () => {}} + complete={async () => {}} + {...props} + />, + ); + }, + + renderSecondFactor(id, props = {}) { + const factor = getSecondFactorUI(id) ?? missing("a second factor", id); + const Component = factor.component; + return wrap( + {}} + challenge={async () => null} + cancel={() => {}} + {...props} + />, + ); + }, + + renderEnrollment(id) { + const entry = getSecondFactorUI(id) ?? getLoginMethodUI(id); + const Component = + entry?.enrollment ?? missing("an enrolment section", id); + return wrap(); + }, + renderSlot(slotId, props = {}) { return wrap( <> diff --git a/src/ui/sidebar/AdminManagementSections.tsx b/src/ui/sidebar/AdminManagementSections.tsx index c3371435c..6c2f08fb9 100644 --- a/src/ui/sidebar/AdminManagementSections.tsx +++ b/src/ui/sidebar/AdminManagementSections.tsx @@ -36,7 +36,7 @@ export type AdminUser = { isOidc: boolean; passwordHash?: string; dataUnlocked?: boolean; - totpEnabled?: boolean; + secondFactorEnabled?: boolean; }; export type AdminSession = { diff --git a/src/ui/sidebar/AdminSecondFactorsSection.tsx b/src/ui/sidebar/AdminSecondFactorsSection.tsx index 11aa6e6a3..9e44c0550 100644 --- a/src/ui/sidebar/AdminSecondFactorsSection.tsx +++ b/src/ui/sidebar/AdminSecondFactorsSection.tsx @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { ShieldOff } from "lucide-react"; import { Button } from "@/components/button"; +import { pluginKey } from "@/lib/plugin-i18n"; import { getUserSecondFactors, resetUserSecondFactors, @@ -70,7 +71,12 @@ export function AdminSecondFactorsSection({ > {factor.labelKey - ? t(factor.labelKey, { defaultValue: factor.factorId }) + ? t( + factor.pluginId === "core" + ? factor.labelKey + : pluginKey(factor.pluginId, factor.labelKey), + { defaultValue: factor.factorId }, + ) : factor.factorId} {!factor.available && ( diff --git a/src/ui/sidebar/AdminSettingsPanel.tsx b/src/ui/sidebar/AdminSettingsPanel.tsx index 3be884c87..0440d3392 100644 --- a/src/ui/sidebar/AdminSettingsPanel.tsx +++ b/src/ui/sidebar/AdminSettingsPanel.tsx @@ -306,7 +306,7 @@ export function AdminSettingsPanel({ isOidc: user.is_oidc, passwordHash: user.password_hash, dataUnlocked: user.data_unlocked, - totpEnabled: user.totp_enabled, + secondFactorEnabled: user.second_factor_enabled, })), ); setUserTotal(total ?? u.length); @@ -989,10 +989,10 @@ export function AdminSettingsPanel({ setUsers((prev) => prev.filter((u) => u.id !== manageUser.id)); setManageUser(null); }} - onTotpDisabled={() => { + onSecondFactorsReset={() => { setUsers((prev) => prev.map((u) => - u.id === manageUser.id ? { ...u, totpEnabled: false } : u, + u.id === manageUser.id ? { ...u, secondFactorEnabled: false } : u, ), ); }} diff --git a/src/ui/sidebar/AdminUserManagePanel.tsx b/src/ui/sidebar/AdminUserManagePanel.tsx index 7ca88b812..9cd3aa0f8 100644 --- a/src/ui/sidebar/AdminUserManagePanel.tsx +++ b/src/ui/sidebar/AdminUserManagePanel.tsx @@ -11,7 +11,6 @@ import { Pencil, Plus, RefreshCw, - ShieldOff, SquareTerminal, Trash2, TriangleAlert, @@ -30,7 +29,6 @@ import { adminUpdateUserSnippet, adminDeleteUserSnippet, adminResetUserPassword, - adminDisableUserTotp, adminExportUserData, getSessions, revokeSession, @@ -112,14 +110,14 @@ export function AdminUserManagePanel({ onBack, onOpenHostTab, onUserDeleted, - onTotpDisabled, + onSecondFactorsReset, }: { user: AdminUser; roles: Role[]; onBack: () => void; onOpenHostTab?: (host: Host) => void; onUserDeleted: () => void; - onTotpDisabled: () => void; + onSecondFactorsReset: () => void; }) { const { t } = useTranslation(); const [activeTab, setActiveTab] = useState("account"); @@ -172,7 +170,6 @@ export function AdminUserManagePanel({ // Account tab state const [resetPassword, setResetPassword] = useState(""); const [resetLoading, setResetLoading] = useState(false); - const [totpEnabled, setTotpEnabled] = useState(!!user.totpEnabled); const [exportLoading, setExportLoading] = useState(false); const [newKeyName, setNewKeyName] = useState(""); const [newKeyLoading, setNewKeyLoading] = useState(false); @@ -292,17 +289,6 @@ export function AdminUserManagePanel({ } } - async function handleDisableTotp() { - try { - await adminDisableUserTotp(user.id); - setTotpEnabled(false); - onTotpDisabled(); - toast.success(t("admin.totpDisabledSuccess")); - } catch (e) { - toast.error(apiErrorMessage(e, t("admin.totpDisableFailed"))); - } - } - async function handleExport() { setExportLoading(true); try { @@ -556,36 +542,6 @@ export function AdminUserManagePanel({ )}
- {/* TOTP */} -
- {sectionHeading(t("admin.totpSectionTitle"))} -
- - {totpEnabled - ? t("admin.totpStatusEnabled") - : t("admin.totpStatusDisabled")} - - {totpEnabled && ( - - )} -
-
- setConfirmDialog({ message, onConfirm }) } - onReset={() => { - setTotpEnabled(false); - onTotpDisabled(); - }} + onReset={onSecondFactorsReset} /> {/* Roles */} diff --git a/src/ui/sidebar/AuthEnrollmentSections.tsx b/src/ui/sidebar/AuthEnrollmentSections.tsx new file mode 100644 index 000000000..62750d4e7 --- /dev/null +++ b/src/ui/sidebar/AuthEnrollmentSections.tsx @@ -0,0 +1,32 @@ +import { useTranslation } from "react-i18next"; +import { useLoginMethods, useSecondFactors } from "@/plugin-host/auth-registry"; + +/** + * Enrolment sections login methods and second factors register, such as + * adding a passkey or setting up an authenticator app. + */ +export function AuthEnrollmentSections() { + const { t } = useTranslation(); + const sections = [...useLoginMethods(), ...useSecondFactors()].filter( + (entry) => entry.enrollment, + ); + if (sections.length === 0) return null; + return ( +
+ {sections.map((entry) => { + const Enrollment = entry.enrollment!; + return ( +
+ + {t(entry.titleKey)} + + +
+ ); + })} +
+ ); +} diff --git a/src/ui/sidebar/PluginSecondFactorEnrollment.tsx b/src/ui/sidebar/PluginSecondFactorEnrollment.tsx deleted file mode 100644 index e5453fadd..000000000 --- a/src/ui/sidebar/PluginSecondFactorEnrollment.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { useSecondFactors } from "@/plugin-host/auth-registry"; - -/** - * Enrolment sections second-factor plugins register. Core's own TOTP and - * passkey sections sit above this until they move into plugins. - */ -export function PluginSecondFactorEnrollment() { - const { t } = useTranslation(); - const factors = useSecondFactors().filter( - (factor) => factor.enrollment && factor.pluginId !== "core", - ); - if (factors.length === 0) return null; - return ( -
- {factors.map((factor) => { - const Enrollment = factor.enrollment!; - return ( -
- - {t(factor.titleKey)} - - -
- ); - })} -
- ); -} diff --git a/src/ui/sidebar/UserProfilePanel.tsx b/src/ui/sidebar/UserProfilePanel.tsx index 4c5560aaa..937affdcc 100644 --- a/src/ui/sidebar/UserProfilePanel.tsx +++ b/src/ui/sidebar/UserProfilePanel.tsx @@ -1,5 +1,5 @@ import { useState, useRef, useEffect } from "react"; -import { PluginSecondFactorEnrollment } from "./PluginSecondFactorEnrollment"; +import { AuthEnrollmentSections } from "./AuthEnrollmentSections"; import { useTranslation } from "react-i18next"; import { copyToClipboard } from "@/lib/clipboard"; import { @@ -11,9 +11,6 @@ import { changePassword, deleteAccount, logoutUser, - setupTOTP, - enableTOTP, - disableTOTP, getVersionInfo, releaseUrlFrom, getUserRoles, @@ -23,13 +20,6 @@ import { import { getDatabaseTransferUrl } from "@/lib/database-transfer-url"; import { readRailPreference, setRailPreference } from "./rail-preferences"; import { useHostSidebarPreferences } from "./tree/hooks/useHostSidebarPreferences"; -import { - deleteWebAuthnCredential, - listWebAuthnCredentials, - registerWebAuthnCredential, - type WebAuthnCredentialSummary, - type WebAuthnUserVerification, -} from "@/api/webauthn-api"; import type { UserRole } from "@/main-axios"; import type React from "react"; import { isElectron } from "@/lib/electron"; @@ -48,7 +38,6 @@ import { } from "@/components/dialog"; import { AlertCircle, - CheckCircle2, ChevronDown, Copy, Database, @@ -65,7 +54,6 @@ import { Trash2, Type, User, - X, } from "lucide-react"; import { SettingRow, FakeSwitch } from "@/components/section-card"; import { NavigationVisibilityToggles } from "./NavigationVisibilityToggles"; @@ -584,27 +572,6 @@ export function UserProfilePanel({ const [isOidc, setIsOidc] = useState(false); const [isDualAuth, setIsDualAuth] = useState(false); - // TOTP - const [totpEnabled, setTotpEnabled] = useState(false); - const [totpStep, setTotpStep] = useState< - "idle" | "setup" | "verify" | "backup" - >("idle"); - const [totpQrCode, setTotpQrCode] = useState(""); - const [totpSecret, setTotpSecret] = useState(""); - const [totpCode, setTotpCode] = useState(""); - const [totpBackupCodes, setTotpBackupCodes] = useState([]); - const [totpLoading, setTotpLoading] = useState(false); - const [showDisableTotp, setShowDisableTotp] = useState(false); - const [disableTotpInput, setDisableTotpInput] = useState(""); - const [showAddTotp, setShowAddTotp] = useState(false); - const [addTotpInput, setAddTotpInput] = useState(""); - const [addingTotpAuthenticator, setAddingTotpAuthenticator] = useState(false); - const [passkeys, setPasskeys] = useState([]); - const [passkeyLoading, setPasskeyLoading] = useState(false); - const [passkeyName, setPasskeyName] = useState(""); - const [passkeyUserVerification, setPasskeyUserVerification] = - useState("preferred"); - // Delete account const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [deletePassword, setDeletePassword] = useState(""); @@ -760,7 +727,6 @@ export function UserProfilePanel({ getUserInfo() .then(async (localInfo) => { setUserId(localInfo.userId); - setTotpEnabled(localInfo.totp_enabled ?? false); setIsOidc(localInfo.is_oidc ?? false); setIsDualAuth(localInfo.is_dual_auth ?? false); const remoteInfo = await getRemoteSyncUserInfo(); @@ -791,9 +757,6 @@ export function UserProfilePanel({ getApiKeys() .then(({ apiKeys: keys }) => setApiKeys(keys)) .catch(() => {}); - listWebAuthnCredentials() - .then(({ credentials }) => setPasskeys(credentials ?? [])) - .catch(() => {}); getVersionInfo() .then((info) => { setVersion(info.localVersion); @@ -1204,133 +1167,6 @@ export function UserProfilePanel({ }); } - async function handleStartTotpSetup() { - setTotpLoading(true); - try { - const result = await setupTOTP(); - setTotpQrCode(result.qr_code); - setTotpSecret(result.secret); - setTotpCode(""); - setTotpStep("setup"); - } catch { - toast.error(t("newUi.sidebar.userProfile.totpSetupFailed")); - } finally { - setTotpLoading(false); - } - } - - async function handleAddTotpAuthenticator() { - if (!addTotpInput) { - toast.error(t("newUi.sidebar.userProfile.totpAddInputRequired")); - return; - } - setTotpLoading(true); - try { - const result = await setupTOTP(addTotpInput); - setTotpQrCode(result.qr_code); - setTotpSecret(result.secret); - setAddingTotpAuthenticator(true); - setShowAddTotp(false); - setAddTotpInput(""); - setTotpStep("setup"); - } catch (e: unknown) { - toast.error( - apiErrorMessage(e, t("newUi.sidebar.userProfile.totpAddFailed")), - ); - } finally { - setTotpLoading(false); - } - } - - async function handleVerifyTotp() { - if (!totpCode || totpCode.length !== 6) { - toast.error(t("newUi.sidebar.userProfile.totpEnter6Digits")); - return; - } - setTotpLoading(true); - try { - const result = await enableTOTP(totpCode); - setTotpBackupCodes(result.backup_codes ?? []); - setTotpEnabled(true); - if (!isRemoteSyncConnected) setAccountTotpEnabled(true); - setTotpStep("backup"); - toast.success(t("newUi.sidebar.userProfile.totpEnabledSuccess")); - } catch (e: unknown) { - toast.error( - apiErrorMessage(e, t("newUi.sidebar.userProfile.totpInvalidCode")), - ); - } finally { - setTotpLoading(false); - } - } - - async function handleDisableTotp() { - if (!disableTotpInput) { - toast.error(t("newUi.sidebar.userProfile.totpDisableInputRequired")); - return; - } - setTotpLoading(true); - try { - await disableTOTP(disableTotpInput); - setTotpEnabled(false); - if (!isRemoteSyncConnected) setAccountTotpEnabled(false); - setShowDisableTotp(false); - setDisableTotpInput(""); - toast.success(t("newUi.sidebar.userProfile.totpDisabledSuccess")); - } catch (e: unknown) { - toast.error( - apiErrorMessage(e, t("newUi.sidebar.userProfile.totpDisableFailed")), - ); - } finally { - setTotpLoading(false); - } - } - - async function handleRegisterPasskey() { - setPasskeyLoading(true); - try { - await registerWebAuthnCredential( - passkeyName || "Passkey", - passkeyUserVerification, - ); - const { credentials } = await listWebAuthnCredentials(); - setPasskeys(credentials ?? []); - setPasskeyName(""); - toast.success(t("newUi.sidebar.userProfile.passkeyAdded")); - } catch (e: unknown) { - toast.error( - apiErrorMessage(e, t("newUi.sidebar.userProfile.passkeyAddFailed")), - ); - } finally { - setPasskeyLoading(false); - } - } - - async function handleDeletePasskey(credentialId: string) { - setPasskeyLoading(true); - try { - await deleteWebAuthnCredential(credentialId); - setPasskeys((prev) => prev.filter((item) => item.id !== credentialId)); - toast.success(t("newUi.sidebar.userProfile.passkeyDeleted")); - } catch (e: unknown) { - toast.error( - apiErrorMessage(e, t("newUi.sidebar.userProfile.passkeyDeleteFailed")), - ); - } finally { - setPasskeyLoading(false); - } - } - - function downloadBackupCodes() { - const blob = new Blob([totpBackupCodes.join("\n")], { type: "text/plain" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = "termix-backup-codes.txt"; - a.click(); - URL.revokeObjectURL(url); - } - async function handleDeleteAccount() { if (!deletePassword.trim()) { toast.error(t("newUi.sidebar.userProfile.deletePasswordRequired")); @@ -2173,395 +2009,7 @@ export function UserProfilePanel({ onToggle={() => toggle("security")} >
-
-
-
-
- - {t("newUi.sidebar.userProfile.totpAuthenticator")} - - - {t("hosts.docsLink")} - -
- - {totpEnabled - ? t("newUi.sidebar.userProfile.totpEnabled") - : t("newUi.sidebar.userProfile.totpDisabled")} - -
- {totpEnabled ? ( -
- - -
- ) : ( - - )} -
- - {totpEnabled && showAddTotp && ( -
- - {t("newUi.sidebar.userProfile.totpAddTitle")} - - - {t("newUi.sidebar.userProfile.totpAddDescription")} - - setAddTotpInput(e.target.value)} - onKeyDown={(e) => - e.key === "Enter" && handleAddTotpAuthenticator() - } - className="text-sm" - /> -
- - -
-
- )} - - {/* Disable TOTP form */} - {totpEnabled && showDisableTotp && ( -
- - {t("newUi.sidebar.userProfile.totpDisableTitle")} - - setDisableTotpInput(e.target.value)} - className="text-sm" - /> -
- - -
-
- )} - - {/* TOTP setup: scan QR */} - {totpStep === "setup" && ( -
-
- - {t("newUi.sidebar.userProfile.setupTotp")} - - -
- {totpQrCode ? ( -
- {t("newUi.sidebar.userProfile.qrCode")} -
- ) : ( -
-
- {t("newUi.sidebar.userProfile.qrCode")} -
-
- )} -
- - {totpSecret} - - -
- - {t( - addingTotpAuthenticator - ? "newUi.sidebar.userProfile.totpAddScanInstructions" - : "newUi.sidebar.userProfile.totpInstructions", - )} - - -
- )} - - {/* TOTP setup: verify code */} - {!totpEnabled && totpStep === "verify" && ( -
-
- - {t("newUi.sidebar.userProfile.totpVerifyTitle")} - - -
- - setTotpCode(e.target.value.replace(/\D/g, "").slice(0, 6)) - } - onKeyDown={(e) => e.key === "Enter" && handleVerifyTotp()} - className="text-center font-mono tracking-widest text-lg h-10" - maxLength={6} - /> -
- - -
-
- )} - - {/* TOTP setup: backup codes */} - {totpStep === "backup" && totpBackupCodes.length > 0 && ( -
- - {t("newUi.sidebar.userProfile.totpBackupTitle")} - -
- {totpBackupCodes.map((code) => ( - - {code} - - ))} -
- - -
- )} -
- -
-
-
- - {t("newUi.sidebar.userProfile.passkeys")} - - - {t("newUi.sidebar.userProfile.passkeysDesc")} - -
-
- -
- setPasskeyName(e.target.value)} - className="h-8 text-xs" - disabled={passkeyLoading} - /> - - setPasskeyUserVerification( - e.target.value as WebAuthnUserVerification, - ) - } - className="h-8 w-28 text-xs border border-border bg-background px-2 outline-none focus:ring-1 focus:ring-ring" - disabled={passkeyLoading} - > - - - - -
- - -
- {passkeys.length === 0 ? ( -
- {t("newUi.sidebar.userProfile.noPasskeys")} -
- ) : ( - passkeys.map((passkey) => ( -
-
- - {passkey.name} - - - {passkey.deviceType || "unknown"} - {passkey.backedUp ? " / synced" : ""} - -
- -
- )) - )} -
-
- - + {canChangePasword && ( ({ - get: vi.fn(async () => ({ data: {} })), - post: vi.fn(async () => ({ data: {} })), - delete: vi.fn(async () => ({ data: { success: true } })), -})); - -const browserMock = vi.hoisted(() => ({ - startAuthentication: vi.fn(async () => ({ id: "cred-1" })), - startRegistration: vi.fn(async () => ({ id: "cred-1" })), - browserSupportsWebAuthn: vi.fn(() => true), -})); - -vi.mock("@/main-axios", () => ({ - authApi: authApiMock, - handleApiError: (error: unknown) => error, -})); - -vi.mock("@simplewebauthn/browser", () => browserMock); - -import { isPasskeySupported, loginWithPasskey } from "../../api/webauthn-api"; - -beforeEach(() => { - authApiMock.get.mockClear(); - authApiMock.post.mockClear(); - authApiMock.delete.mockClear(); - browserMock.startAuthentication.mockClear(); - browserMock.browserSupportsWebAuthn.mockClear(); -}); - -describe("isPasskeySupported", () => { - it("reports what the browser helper returns", () => { - browserMock.browserSupportsWebAuthn.mockReturnValueOnce(false); - expect(isPasskeySupported()).toBe(false); - browserMock.browserSupportsWebAuthn.mockReturnValueOnce(true); - expect(isPasskeySupported()).toBe(true); - }); -}); - -describe("loginWithPasskey", () => { - it("passes the challenge from options through to verify", async () => { - authApiMock.post - .mockResolvedValueOnce({ - data: { options: { challenge: "abc" }, challengeId: "chal-1" }, - }) - .mockResolvedValueOnce({ data: { success: true, username: "luke" } }); - - const result = await loginWithPasskey("luke", true); - - expect(authApiMock.post).toHaveBeenNthCalledWith( - 1, - "/users/webauthn/authenticate/options", - { username: "luke" }, - ); - expect(browserMock.startAuthentication).toHaveBeenCalledWith({ - optionsJSON: { challenge: "abc" }, - }); - expect(authApiMock.post).toHaveBeenNthCalledWith( - 2, - "/users/webauthn/authenticate/verify", - { - challengeId: "chal-1", - response: { id: "cred-1" }, - rememberMe: true, - }, - ); - expect(result).toEqual({ success: true, username: "luke" }); - }); - - it("omits the username so discoverable passkeys work", async () => { - authApiMock.post - .mockResolvedValueOnce({ - data: { options: { challenge: "abc" }, challengeId: "chal-2" }, - }) - .mockResolvedValueOnce({ data: { success: true } }); - - await loginWithPasskey(); - - expect(authApiMock.post).toHaveBeenNthCalledWith( - 1, - "/users/webauthn/authenticate/options", - {}, - ); - expect(authApiMock.post).toHaveBeenNthCalledWith( - 2, - "/users/webauthn/authenticate/verify", - expect.objectContaining({ rememberMe: false }), - ); - }); - - it("returns the totp challenge instead of a session", async () => { - authApiMock.post - .mockResolvedValueOnce({ - data: { options: { challenge: "abc" }, challengeId: "chal-3" }, - }) - .mockResolvedValueOnce({ - data: { success: true, requires_totp: true, temp_token: "tmp" }, - }); - - const result = await loginWithPasskey("luke"); - - expect(result.requires_totp).toBe(true); - expect(result.temp_token).toBe("tmp"); - }); - - it("surfaces a cancelled prompt to the caller", async () => { - authApiMock.post.mockResolvedValueOnce({ - data: { options: { challenge: "abc" }, challengeId: "chal-4" }, - }); - const cancelled = Object.assign(new Error("cancelled"), { - name: "NotAllowedError", - }); - browserMock.startAuthentication.mockRejectedValueOnce(cancelled); - - await expect(loginWithPasskey("luke")).rejects.toMatchObject({ - name: "NotAllowedError", - }); - expect(authApiMock.post).toHaveBeenCalledTimes(1); - }); -}); diff --git a/src/ui/tests/auth/Auth.test.tsx b/src/ui/tests/auth/Auth.test.tsx index 0210f16d4..2faccdbd9 100644 --- a/src/ui/tests/auth/Auth.test.tsx +++ b/src/ui/tests/auth/Auth.test.tsx @@ -22,7 +22,6 @@ const mainAxios = vi.hoisted(() => ({ verifyPasswordResetCode: vi.fn(), completePasswordReset: vi.fn(), getOIDCAuthorizeUrl: vi.fn(), - verifyTOTPLogin: vi.fn(), isElectron: vi.fn(), getCurrentToken: vi.fn(), getOidcSilentLoginDefault: vi.fn(), diff --git a/src/ui/tests/auth/AuthEmbeddedServerFailure.test.tsx b/src/ui/tests/auth/AuthEmbeddedServerFailure.test.tsx index e12dd6a37..58365b70f 100644 --- a/src/ui/tests/auth/AuthEmbeddedServerFailure.test.tsx +++ b/src/ui/tests/auth/AuthEmbeddedServerFailure.test.tsx @@ -32,7 +32,6 @@ vi.mock("@/main-axios", () => ({ verifyPasswordResetCode: vi.fn(), completePasswordReset: vi.fn(), getOIDCAuthorizeUrl: vi.fn(), - verifyTOTPLogin: vi.fn(), getCurrentToken: vi.fn().mockReturnValue(null), requestTrustedProxyLogin: vi.fn(), })); @@ -42,11 +41,6 @@ vi.mock("@/api/sso-provider-api", () => ({ ldapLogin: vi.fn(), })); -vi.mock("@/api/webauthn-api", () => ({ - isPasskeySupported: () => false, - loginWithPasskey: vi.fn(), -})); - import { Auth } from "@/auth/Auth"; function setEmbeddedServerStatus(failure: unknown) { diff --git a/src/ui/tests/auth/AuthLoginMethods.test.tsx b/src/ui/tests/auth/AuthLoginMethods.test.tsx index 8f3c62f7e..16ba42a79 100644 --- a/src/ui/tests/auth/AuthLoginMethods.test.tsx +++ b/src/ui/tests/auth/AuthLoginMethods.test.tsx @@ -246,23 +246,69 @@ describe("login methods on the login screen", () => { }); it("picks up the second factor step after a redirect login", async () => { - window.history.replaceState( - {}, - "", - "/?second_factor=1&second_factors=totp", - ); - render(); - const input = await screen.findByPlaceholderText("000000"); - // Backup codes are letters and digits, and are accepted now. - fireEvent.change(input, { target: { value: "abcd-2345" } }); - expect((input as HTMLInputElement).value).toBe("ABCD2345"); + const dispose = registerSecondFactor({ + id: "pin", + pluginId: "corp", + titleKey: "corp.pin", + component: ({ verify }: SecondFactorUIProps) => ( + + ), + }); + window.history.replaceState({}, "", "/?second_factor=1&second_factors=pin"); authMethodsApi.verifySecondFactor.mockResolvedValue({ success: true }); - fireEvent.submit(input.closest("form")!); - await waitFor(() => - expect(authMethodsApi.verifySecondFactor).toHaveBeenCalledWith("totp", { - totp_code: "ABCD2345", - rememberMe: false, - }), - ); + try { + render(); + fireEvent.click(await screen.findByText("send pin")); + await waitFor(() => + expect(authMethodsApi.verifySecondFactor).toHaveBeenCalledWith("pin", { + pin: "1234", + rememberMe: false, + }), + ); + } finally { + dispose(); + } + }); + + it("offers every registered factor when the server lists none", async () => { + const dispose = registerSecondFactor({ + id: "pin", + pluginId: "corp", + titleKey: "corp.pin", + component: () => pin challenge, + }); + window.history.replaceState({}, "", "/?second_factor=1"); + try { + render(); + expect(await screen.findByText("pin challenge")).toBeTruthy(); + } finally { + dispose(); + } + }); + + it("draws an inline method under the password form, not in the external list", async () => { + authMethodsApi.getLoginMethods.mockResolvedValue([ + { + id: "quick-key", + pluginId: "keys", + kind: "form", + labelKey: "keys.title", + instances: [], + }, + ]); + const dispose = registerLoginMethod({ + id: "quick-key", + pluginId: "keys", + titleKey: "keys.title", + placement: "inline", + component: () => , + }); + try { + render(); + expect(await screen.findByText("quick key")).toBeTruthy(); + expect(screen.queryByText("auth.external")).toBeNull(); + } finally { + dispose(); + } }); }); diff --git a/src/ui/tests/auth/ElectronLoginForm.test.tsx b/src/ui/tests/auth/ElectronLoginForm.test.tsx index 3433cb152..65f51ce48 100644 --- a/src/ui/tests/auth/ElectronLoginForm.test.tsx +++ b/src/ui/tests/auth/ElectronLoginForm.test.tsx @@ -26,8 +26,7 @@ describe("ElectronLoginForm", () => { it.each([ "auth_component", - "totp_auth_component", - "passkey_auth_component", + "second_factor_auth_component", "method_auth_component", ])("accepts a login hand-off from %s", async (source) => { const onAuthSuccess = vi.fn(); diff --git a/src/ui/tests/sidebar/AdminSecondFactorsSection.test.tsx b/src/ui/tests/sidebar/AdminSecondFactorsSection.test.tsx index 967d46e28..fce81cbf1 100644 --- a/src/ui/tests/sidebar/AdminSecondFactorsSection.test.tsx +++ b/src/ui/tests/sidebar/AdminSecondFactorsSection.test.tsx @@ -22,9 +22,9 @@ describe("AdminSecondFactorsSection", () => { it("shows a factor whose plugin is off and resets after confirming", async () => { api.getUserSecondFactors.mockResolvedValue([ { - pluginId: "core", + pluginId: "totp", factorId: "totp", - labelKey: "auth.totpFactor", + labelKey: "factor", available: true, }, { pluginId: "yubi", factorId: "otp", labelKey: null, available: false }, @@ -46,6 +46,8 @@ describe("AdminSecondFactorsSection", () => { expect( await screen.findByText("admin.secondFactorUnavailable:yubi"), ).toBeTruthy(); + // A plugin's label resolves in its own namespace. + expect(screen.getByText("totp:factor")).toBeTruthy(); fireEvent.click(screen.getByText("admin.resetSecondFactors")); expect(requestConfirm).toHaveBeenCalled(); await waitFor(() => diff --git a/src/ui/tests/sidebar/AdminUserManagePanel.test.tsx b/src/ui/tests/sidebar/AdminUserManagePanel.test.tsx index 08ed49f60..d4bcf9bb2 100644 --- a/src/ui/tests/sidebar/AdminUserManagePanel.test.tsx +++ b/src/ui/tests/sidebar/AdminUserManagePanel.test.tsx @@ -15,7 +15,6 @@ const api = vi.hoisted(() => ({ adminUpdateUserSnippet: vi.fn(async () => ({})), adminDeleteUserSnippet: vi.fn(async () => ({})), adminResetUserPassword: vi.fn(async () => ({ dataWiped: false })), - adminDisableUserTotp: vi.fn(async () => ({})), adminExportUserData: vi.fn(async () => ({})), getSessions: vi.fn(async () => ({ sessions: [] as unknown[] })), revokeSession: vi.fn(async () => ({})), @@ -56,7 +55,6 @@ function makeUser(overrides: Partial = {}): AdminUser { isOidc: false, passwordHash: "hash", dataUnlocked: true, - totpEnabled: false, ...overrides, }; } @@ -72,7 +70,7 @@ function renderPanel( onBack={callbacks.onBack ?? vi.fn()} onOpenHostTab={callbacks.onOpenHostTab as never} onUserDeleted={callbacks.onUserDeleted ?? vi.fn()} - onTotpDisabled={callbacks.onTotpDisabled ?? vi.fn()} + onSecondFactorsReset={callbacks.onSecondFactorsReset ?? vi.fn()} />, ); } @@ -144,21 +142,6 @@ describe("AdminUserManagePanel", () => { }); }); - it("disables TOTP through the confirm dialog", async () => { - const onTotpDisabled = vi.fn(); - renderPanel(makeUser({ totpEnabled: true }), { onTotpDisabled }); - - expect(screen.getByText("admin.totpStatusEnabled")).toBeTruthy(); - await userEvent.click(screen.getByText("admin.disableTotp")); - await userEvent.click(screen.getByText("common.confirm")); - - await waitFor(() => { - expect(api.adminDisableUserTotp).toHaveBeenCalledWith("u2"); - expect(onTotpDisabled).toHaveBeenCalled(); - }); - expect(screen.getByText("admin.totpStatusDisabled")).toBeTruthy(); - }); - it("creates a snippet for the target user", async () => { renderPanel(makeUser());