From 4ef554b9d257a7da515212f7a155e61792d9e3c0 Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Sat, 21 Mar 2026 12:36:32 +0500 Subject: [PATCH] Two-Factor Authentication (2FA) (#10658) * Two-Factor Authentication (2FA) Signed-off-by: Denis Bykhov * Fix test Signed-off-by: Denis Bykhov * Fix test Signed-off-by: Denis Bykhov * Fix tests Signed-off-by: Denis Bykhov * More test fixes Signed-off-by: Denis Bykhov * Fix tests Signed-off-by: Denis Bykhov --------- Signed-off-by: Denis Bykhov --- common/config/rush/pnpm-lock.yaml | 145 +++++++++++++++++ .../packages/account-client/src/client.ts | 42 +++++ .../core/packages/account-client/src/types.ts | 2 + foundations/core/packages/core/src/classes.ts | 1 + models/setting/src/index.ts | 16 ++ plugins/login-assets/lang/cs.json | 14 +- plugins/login-assets/lang/de.json | 14 +- plugins/login-assets/lang/en.json | 6 +- plugins/login-assets/lang/es.json | 14 +- plugins/login-assets/lang/fr.json | 14 +- plugins/login-assets/lang/it.json | 14 +- plugins/login-assets/lang/ja.json | 14 +- plugins/login-assets/lang/pt-br.json | 14 +- plugins/login-assets/lang/pt.json | 14 +- plugins/login-assets/lang/ru.json | 14 +- plugins/login-assets/lang/tr.json | 14 +- plugins/login-assets/lang/zh.json | 14 +- .../src/components/LoginApp.svelte | 8 +- .../src/components/LoginTfaForm.svelte | 65 ++++++++ plugins/login-resources/src/utils.ts | 44 +++++ plugins/login/src/index.ts | 9 +- plugins/setting-assets/lang/cs.json | 11 +- plugins/setting-assets/lang/de.json | 11 +- plugins/setting-assets/lang/en.json | 11 +- plugins/setting-assets/lang/es.json | 11 +- plugins/setting-assets/lang/fr.json | 11 +- plugins/setting-assets/lang/it.json | 11 +- plugins/setting-assets/lang/ja.json | 11 +- plugins/setting-assets/lang/pt-br.json | 11 +- plugins/setting-assets/lang/pt.json | 11 +- plugins/setting-assets/lang/ru.json | 11 +- plugins/setting-assets/lang/tr.json | 11 +- plugins/setting-assets/lang/zh.json | 11 +- plugins/setting-resources/package.json | 6 +- .../src/components/TwoFactorSettings.svelte | 152 ++++++++++++++++++ plugins/setting-resources/src/index.ts | 4 +- plugins/setting/src/index.ts | 15 +- plugins/workbench-resources/src/connect.ts | 2 + server/account/package.json | 1 + .../account/src/__tests__/operations.test.ts | 24 ++- server/account/src/__tests__/postgres.test.ts | 1 + .../src/collections/postgres/migrations.ts | 14 +- .../src/collections/postgres/postgres.ts | 1 + server/account/src/operations.ts | 147 ++++++++++++++++- server/account/src/types.ts | 2 + server/account/src/utils.ts | 13 ++ 46 files changed, 916 insertions(+), 90 deletions(-) create mode 100644 plugins/login-resources/src/components/LoginTfaForm.svelte create mode 100644 plugins/setting-resources/src/components/TwoFactorSettings.svelte diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 8122e73cac..b5ae38fa90 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -26436,6 +26436,9 @@ importers: '@hcengineering/workbench-resources': specifier: workspace:^0.7.0 version: link:../workbench-resources + qrcode: + specifier: ^1.5.4 + version: 1.5.4 svelte: specifier: ^4.2.20 version: 4.2.20 @@ -26446,6 +26449,9 @@ importers: '@types/jest': specifier: ^29.5.5 version: 29.5.14 + '@types/qrcode': + specifier: ^1.5.5 + version: 1.5.6 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) @@ -35776,6 +35782,9 @@ importers: otp-generator: specifier: ^4.0.1 version: 4.0.1 + otplib: + specifier: ^12.0.1 + version: 12.0.1 postgres: specifier: ^3.4.7 version: 3.4.7 @@ -43857,6 +43866,24 @@ packages: peerDependencies: '@opentelemetry/api': ^1.1.0 + '@otplib/core@12.0.1': + resolution: {integrity: sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==} + + '@otplib/plugin-crypto@12.0.1': + resolution: {integrity: sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==} + deprecated: Please upgrade to v13 of otplib. Refer to otplib docs for migration paths + + '@otplib/plugin-thirty-two@12.0.1': + resolution: {integrity: sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==} + deprecated: Please upgrade to v13 of otplib. Refer to otplib docs for migration paths + + '@otplib/preset-default@12.0.1': + resolution: {integrity: sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==} + deprecated: Please upgrade to v13 of otplib. Refer to otplib docs for migration paths + + '@otplib/preset-v11@12.0.1': + resolution: {integrity: sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==} + '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} @@ -44971,6 +44998,9 @@ packages: '@types/pug@2.0.10': resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==} + '@types/qrcode@1.5.6': + resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} + '@types/qs@6.9.18': resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==} @@ -45973,6 +46003,9 @@ packages: cliui@4.1.0: resolution: {integrity: sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==} + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -46700,6 +46733,9 @@ packages: resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==} engines: {node: '>=0.3.1'} + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + dingbat-to-unicode@1.0.1: resolution: {integrity: sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==} @@ -49602,6 +49638,9 @@ packages: resolution: {integrity: sha512-2TJ52vUftA0+J3eque4wwVtpaL4/NdIXDL0gFWFJFVUAZwAN7+9tltMhL7GCNYaHJtuONoier8Hayyj4HLbSag==} engines: {node: '>=14.10.0'} + otplib@12.0.1: + resolution: {integrity: sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==} + own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} @@ -49917,6 +49956,10 @@ packages: resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==} engines: {node: '>=4.0.0'} + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + points-on-curve@0.2.0: resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} @@ -50214,6 +50257,11 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + qs@6.11.2: resolution: {integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==} engines: {node: '>=0.6'} @@ -50355,6 +50403,9 @@ packages: require-main-filename@1.0.1: resolution: {integrity: sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==} + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} @@ -51172,6 +51223,10 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thirty-two@1.0.2: + resolution: {integrity: sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==} + engines: {node: '>=0.2.6'} + through2@4.0.2: resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} @@ -51819,6 +51874,10 @@ packages: resolution: {integrity: sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==} engines: {node: '>=0.10.0'} + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -51948,6 +52007,10 @@ packages: yargs-parser@11.1.1: resolution: {integrity: sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==} + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -51955,6 +52018,10 @@ packages: yargs@12.0.5: resolution: {integrity: sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==} + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} @@ -54795,6 +54862,29 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@otplib/core@12.0.1': {} + + '@otplib/plugin-crypto@12.0.1': + dependencies: + '@otplib/core': 12.0.1 + + '@otplib/plugin-thirty-two@12.0.1': + dependencies: + '@otplib/core': 12.0.1 + thirty-two: 1.0.2 + + '@otplib/preset-default@12.0.1': + dependencies: + '@otplib/core': 12.0.1 + '@otplib/plugin-crypto': 12.0.1 + '@otplib/plugin-thirty-two': 12.0.1 + + '@otplib/preset-v11@12.0.1': + dependencies: + '@otplib/core': 12.0.1 + '@otplib/plugin-crypto': 12.0.1 + '@otplib/plugin-thirty-two': 12.0.1 + '@paralleldrive/cuid2@2.3.1': dependencies: '@noble/hashes': 1.8.0 @@ -56120,6 +56210,10 @@ snapshots: '@types/pug@2.0.10': {} + '@types/qrcode@1.5.6': + dependencies: + '@types/node': 22.19.0 + '@types/qs@6.9.18': {} '@types/querystringify@2.0.2': {} @@ -57322,6 +57416,12 @@ snapshots: strip-ansi: 4.0.0 wrap-ansi: 2.1.0 + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -58062,6 +58162,8 @@ snapshots: diff@7.0.0: {} + dijkstrajs@1.0.3: {} + dingbat-to-unicode@1.0.1: {} dir-compare@4.2.0: @@ -61660,6 +61762,12 @@ snapshots: otp-generator@4.0.1: {} + otplib@12.0.1: + dependencies: + '@otplib/core': 12.0.1 + '@otplib/preset-default': 12.0.1 + '@otplib/preset-v11': 12.0.1 + own-keys@1.0.1: dependencies: get-intrinsic: 1.3.0 @@ -61960,6 +62068,8 @@ snapshots: pngjs@3.4.0: {} + pngjs@5.0.0: {} + points-on-curve@0.2.0: {} points-on-path@0.2.1: @@ -62319,6 +62429,12 @@ snapshots: pure-rand@6.1.0: {} + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + qs@6.11.2: dependencies: side-channel: 1.1.0 @@ -62478,6 +62594,8 @@ snapshots: require-main-filename@1.0.1: {} + require-main-filename@2.0.0: {} + requires-port@1.0.0: {} resedit@1.7.2: @@ -63505,6 +63623,8 @@ snapshots: dependencies: any-promise: 1.3.0 + thirty-two@1.0.2: {} + through2@4.0.2: dependencies: readable-stream: 3.6.2 @@ -64357,6 +64477,12 @@ snapshots: string-width: 1.0.2 strip-ansi: 3.0.1 + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -64463,6 +64589,11 @@ snapshots: camelcase: 5.3.1 decamelize: 1.2.0 + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + yargs-parser@21.1.1: {} yargs@12.0.5: @@ -64480,6 +64611,20 @@ snapshots: y18n: 4.0.3 yargs-parser: 11.1.1 + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + yargs@17.7.2: dependencies: cliui: 8.0.1 diff --git a/foundations/core/packages/account-client/src/client.ts b/foundations/core/packages/account-client/src/client.ts index f58806f170..1a8e6ea430 100644 --- a/foundations/core/packages/account-client/src/client.ts +++ b/foundations/core/packages/account-client/src/client.ts @@ -254,8 +254,14 @@ export interface AccountClient { getWorkspacePermissions: (params: { accountId: AccountUuid, permission: string }) => Promise getWorkspaceUsersWithPermission: (params: { permission: string }) => Promise + verify2fa: (code: string) => Promise + setCookie: () => Promise deleteCookie: () => Promise + + generate2faSecret: () => Promise<{ secret: string, url: string }> + enable2fa: (secret: string, code: string) => Promise + disable2fa: (code: string) => Promise } /** @public */ @@ -1335,6 +1341,42 @@ class AccountClientImpl implements AccountClient { params }) } + + async verify2fa (code: string): Promise { + const request = { + method: 'verify2fa' as const, + params: { code } + } + + return await this.rpc(request) + } + + async generate2faSecret (): Promise<{ secret: string, url: string }> { + const request = { + method: 'generate2faSecret' as const, + params: {} + } + + return await this.rpc(request) + } + + async enable2fa (secret: string, code: string): Promise { + const request = { + method: 'enable2fa' as const, + params: { secret, code } + } + + await this.rpc(request) + } + + async disable2fa (code: string): Promise { + const request = { + method: 'disable2fa' as const, + params: { code } + } + + await this.rpc(request) + } } function withRetry Promise> ( diff --git a/foundations/core/packages/account-client/src/types.ts b/foundations/core/packages/account-client/src/types.ts index f055a74d70..c9f791f3c2 100644 --- a/foundations/core/packages/account-client/src/types.ts +++ b/foundations/core/packages/account-client/src/types.ts @@ -19,6 +19,8 @@ export interface LoginInfo { name?: string socialId?: PersonId token?: string + tfaRequired?: boolean + extra?: Record } export interface EndpointInfo { diff --git a/foundations/core/packages/core/src/classes.ts b/foundations/core/packages/core/src/classes.ts index 50664bfabf..2551c951cd 100644 --- a/foundations/core/packages/core/src/classes.ts +++ b/foundations/core/packages/core/src/classes.ts @@ -960,6 +960,7 @@ export interface SocialId { export interface AccountInfo { timezone?: string locale?: string + tfaEnabled?: boolean } export type SocialKey = Pick diff --git a/models/setting/src/index.ts b/models/setting/src/index.ts index 3f6e1a7d86..09a7404caf 100644 --- a/models/setting/src/index.ts +++ b/models/setting/src/index.ts @@ -209,6 +209,22 @@ export function createModel (builder: Builder): void { }, setting.ids.Password ) + + builder.createDoc( + setting.class.SettingsCategory, + core.space.Model, + { + name: 'security', + label: setting.string.Security, + icon: setting.icon.Password, + component: setting.component.TwoFactorSettings, + group: 'settings-account', + role: AccountRole.Guest, + order: 1200 + }, + setting.ids.Security + ) + builder.createDoc( setting.class.SettingsCategory, core.space.Model, diff --git a/plugins/login-assets/lang/cs.json b/plugins/login-assets/lang/cs.json index ba74a3ec3f..c9279bbc50 100644 --- a/plugins/login-assets/lang/cs.json +++ b/plugins/login-assets/lang/cs.json @@ -93,10 +93,14 @@ "SignUpAndJoin": "Registrovat se a připojit", "CreateNewAccount": "Vytvořit nový účet", "SignedInAs": "Přihlášen jako {name}", - "SetPassword": "Set a password", - "SSOPasswordDescription": "Your account uses external login. Set up a password via email confirmation.", - "SendSetupLink": "Send setup link", - "SSOPasswordEmailSent": "Check your email for a link to set your password.", - "SSONoEmailLinked": "No email address is linked to your account. Add one in Account Settings → Manage Identities first." + "TwoFactorAuth": "Dvoufaktorové ověření", + "EnterTwoFactorCode": "Zadejte dvoufaktorový ověřovací kód", + "TwoFactorCode": "Dvoufaktorový ověřovací kód", + "Verify": "Ověřit", + "SetPassword": "Nastavit heslo", + "SSOPasswordDescription": "Váš účet používá externí přihlášení. Nastavte si heslo prostřednictvím e-mailové potvrzení.", + "SendSetupLink": "Odeslat odkaz na nastavení", + "SSOPasswordEmailSent": "Zkontrolujte svůj e-mail, zda neobsahuje odkaz pro nastavení hesla.", + "SSONoEmailLinked": "K vašemu účtu není přidružena žádná e-mailová adresa. Nejprve ji přidejte v Nastavení účtu → Spravovat identity." } } diff --git a/plugins/login-assets/lang/de.json b/plugins/login-assets/lang/de.json index 721b9e788c..76bc7d3561 100644 --- a/plugins/login-assets/lang/de.json +++ b/plugins/login-assets/lang/de.json @@ -93,10 +93,14 @@ "SignUpAndJoin": "Registrieren & beitreten", "CreateNewAccount": "Neues Konto erstellen", "SignedInAs": "Angemeldet als {name}", - "SetPassword": "Set a password", - "SSOPasswordDescription": "Your account uses external login. Set up a password via email confirmation.", - "SendSetupLink": "Send setup link", - "SSOPasswordEmailSent": "Check your email for a link to set your password.", - "SSONoEmailLinked": "No email address is linked to your account. Add one in Account Settings → Manage Identities first." + "TwoFactorAuth": "Zweistufige Authentifizierung", + "EnterTwoFactorCode": "Geben Sie den zweistufigen Authentifizierungscode ein", + "TwoFactorCode": "Zweistufiger Authentifizierungscode", + "Verify": "Überprüfen", + "SetPassword": "Passwort festlegen", + "SSOPasswordDescription": "Ihr Konto verwendet externe Anmeldung. Richten Sie ein Passwort über die E-Mail-Bestätigung ein.", + "SendSetupLink": "Setup-Link senden", + "SSOPasswordEmailSent": "Überprüfen Sie Ihre E-Mail auf einen Link zum Festlegen des Passworts.", + "SSONoEmailLinked": "Keine E-Mail-Adresse mit Ihrem Konto verknüpft. Fügen Sie eine unter Kontoeinstellungen → Identitäten verwalten hinzu." } } diff --git a/plugins/login-assets/lang/en.json b/plugins/login-assets/lang/en.json index 491e9f4a01..c16dd1c152 100644 --- a/plugins/login-assets/lang/en.json +++ b/plugins/login-assets/lang/en.json @@ -96,6 +96,10 @@ "LogInAndJoin": "Log In & Join", "SignUpAndJoin": "Sign Up & Join", "CreateNewAccount": "Create new account", - "SignedInAs": "Signed in as {name}" + "SignedInAs": "Signed in as {name}", + "TwoFactorAuth": "Two-factor authentication", + "EnterTwoFactorCode": "Enter two-factor authentication code", + "TwoFactorCode": "Two-factor authentication code", + "Verify": "Verify" } } diff --git a/plugins/login-assets/lang/es.json b/plugins/login-assets/lang/es.json index 209388928e..b337da57b9 100644 --- a/plugins/login-assets/lang/es.json +++ b/plugins/login-assets/lang/es.json @@ -92,10 +92,14 @@ "SignUpAndJoin": "Registrarse y unirse", "CreateNewAccount": "Crear nueva cuenta", "SignedInAs": "Conectado como {name}", - "SetPassword": "Set a password", - "SSOPasswordDescription": "Your account uses external login. Set up a password via email confirmation.", - "SendSetupLink": "Send setup link", - "SSOPasswordEmailSent": "Check your email for a link to set your password.", - "SSONoEmailLinked": "No email address is linked to your account. Add one in Account Settings → Manage Identities first." + "TwoFactorAuth": "Autenticación de dos factores", + "EnterTwoFactorCode": "Introduce el código de autenticación de dos factores", + "TwoFactorCode": "Código de autenticación de dos factores", + "Verify": "Verificar", + "SetPassword": "Establecer una contraseña", + "SSOPasswordDescription": "Tu cuenta utiliza inicio de sesión externo. Configura una contraseña mediante confirmación por correo electrónico.", + "SendSetupLink": "Enviar enlace de configuración", + "SSOPasswordEmailSent": "Revisa tu correo electrónico para encontrar un enlace para configurar la contraseña.", + "SSONoEmailLinked": "No hay dirección de correo electrónico vinculada a tu cuenta. Añade una en Configuración de la cuenta → Gestionar identidades primero." } } diff --git a/plugins/login-assets/lang/fr.json b/plugins/login-assets/lang/fr.json index ff8906688f..24eedf83a8 100644 --- a/plugins/login-assets/lang/fr.json +++ b/plugins/login-assets/lang/fr.json @@ -92,10 +92,14 @@ "SignUpAndJoin": "S'inscrire et rejoindre", "CreateNewAccount": "Créer un nouveau compte", "SignedInAs": "Connecté en tant que {name}", - "SetPassword": "Set a password", - "SSOPasswordDescription": "Your account uses external login. Set up a password via email confirmation.", - "SendSetupLink": "Send setup link", - "SSOPasswordEmailSent": "Check your email for a link to set your password.", - "SSONoEmailLinked": "No email address is linked to your account. Add one in Account Settings → Manage Identities first." + "TwoFactorAuth": "Authentification à deux facteurs", + "EnterTwoFactorCode": "Entrez le code d'authentification à deux facteurs", + "TwoFactorCode": "Code d'authentification à deux facteurs", + "Verify": "Vérifier", + "SetPassword": "Définir un mot de passe", + "SSOPasswordDescription": "Votre compte utilise une connexion externe. Définissez un mot de passe via confirmation par e-mail.", + "SendSetupLink": "Envoyer le lien de configuration", + "SSOPasswordEmailSent": "Vérifiez votre e-mail pour trouver un lien de configuration de mot de passe.", + "SSONoEmailLinked": "Aucune adresse e-mail n'est liée à votre compte. Ajoutez-en une dans Paramètres du compte → Gérer les identités d'abord." } } diff --git a/plugins/login-assets/lang/it.json b/plugins/login-assets/lang/it.json index ca58d59af6..4d9468bf4a 100644 --- a/plugins/login-assets/lang/it.json +++ b/plugins/login-assets/lang/it.json @@ -92,10 +92,14 @@ "SignUpAndJoin": "Iscriviti e partecipa", "CreateNewAccount": "Crea nuovo account", "SignedInAs": "Accesso effettuato come {name}", - "SetPassword": "Set a password", - "SSOPasswordDescription": "Your account uses external login. Set up a password via email confirmation.", - "SendSetupLink": "Send setup link", - "SSOPasswordEmailSent": "Check your email for a link to set your password.", - "SSONoEmailLinked": "No email address is linked to your account. Add one in Account Settings → Manage Identities first." + "TwoFactorAuth": "Autenticazione a due fattori", + "EnterTwoFactorCode": "Inserisci il codice di autenticazione a due fattori", + "TwoFactorCode": "Codice di autenticazione a due fattori", + "Verify": "Verifica", + "SetPassword": "Imposta una password", + "SSOPasswordDescription": "Il tuo account utilizza l'accesso esterno. Imposta una password tramite conferma via email.", + "SendSetupLink": "Invia link di configurazione", + "SSOPasswordEmailSent": "Controlla la tua email per trovare un link per impostare la password.", + "SSONoEmailLinked": "Nessun indirizzo email è collegato al tuo account. Aggiungine uno in Impostazioni account → Gestisci identità prima." } } diff --git a/plugins/login-assets/lang/ja.json b/plugins/login-assets/lang/ja.json index 74c34d5708..06538a5148 100644 --- a/plugins/login-assets/lang/ja.json +++ b/plugins/login-assets/lang/ja.json @@ -92,10 +92,14 @@ "SignUpAndJoin": "サインアップして参加", "CreateNewAccount": "新規アカウントを作成", "SignedInAs": "{name} としてサインイン中", - "SetPassword": "Set a password", - "SSOPasswordDescription": "Your account uses external login. Set up a password via email confirmation.", - "SendSetupLink": "Send setup link", - "SSOPasswordEmailSent": "Check your email for a link to set your password.", - "SSONoEmailLinked": "No email address is linked to your account. Add one in Account Settings → Manage Identities first." + "TwoFactorAuth": "2段階認証", + "EnterTwoFactorCode": "2段階認証コードを入力", + "TwoFactorCode": "2段階認証コード", + "Verify": "確認", + "SetPassword": "パスワードを設定", + "SSOPasswordDescription": "アカウントは外部ログインを使用しています。メール確認でパスワードを設定してください。", + "SendSetupLink": "セットアップリンクを送信", + "SSOPasswordEmailSent": "パスワードを設定するためのリンクが記載されたメールを確認してください。", + "SSONoEmailLinked": "アカウントにメールアドレスがリンクされていません。まず、アカウント設定 → IDの管理 で追加してください。" } } diff --git a/plugins/login-assets/lang/pt-br.json b/plugins/login-assets/lang/pt-br.json index 51930218be..03a2be87bf 100644 --- a/plugins/login-assets/lang/pt-br.json +++ b/plugins/login-assets/lang/pt-br.json @@ -92,10 +92,14 @@ "SignUpAndJoin": "Cadastrar e participar", "CreateNewAccount": "Criar nova conta", "SignedInAs": "Conectado como {name}", - "SetPassword": "Set a password", - "SSOPasswordDescription": "Your account uses external login. Set up a password via email confirmation.", - "SendSetupLink": "Send setup link", - "SSOPasswordEmailSent": "Check your email for a link to set your password.", - "SSONoEmailLinked": "No email address is linked to your account. Add one in Account Settings → Manage Identities first." + "TwoFactorAuth": "Autenticação de dois fatores", + "EnterTwoFactorCode": "Digite o código de autenticação de dois fatores", + "TwoFactorCode": "Código de autenticação de dois fatores", + "Verify": "Verificar", + "SetPassword": "Definir senha", + "SSOPasswordDescription": "Sua conta usa login externo. Defina uma senha através da confirmação por e-mail.", + "SendSetupLink": "Enviar link de configuração", + "SSOPasswordEmailSent": "Verifique seu e-mail para encontrar o link de configuração da senha.", + "SSONoEmailLinked": "Nenhum endereço de e-mail está vinculado à sua conta. Adicione um em Configurações da Conta → Gerenciar Identidades primeiro." } } diff --git a/plugins/login-assets/lang/pt.json b/plugins/login-assets/lang/pt.json index cc031cf2c6..14e30dbdc1 100644 --- a/plugins/login-assets/lang/pt.json +++ b/plugins/login-assets/lang/pt.json @@ -92,10 +92,14 @@ "SignUpAndJoin": "Registar e participar", "CreateNewAccount": "Criar nova conta", "SignedInAs": "Sessão iniciada como {name}", - "SetPassword": "Set a password", - "SSOPasswordDescription": "Your account uses external login. Set up a password via email confirmation.", - "SendSetupLink": "Send setup link", - "SSOPasswordEmailSent": "Check your email for a link to set your password.", - "SSONoEmailLinked": "No email address is linked to your account. Add one in Account Settings → Manage Identities first." + "TwoFactorAuth": "Autenticação de dois fatores", + "EnterTwoFactorCode": "Digite o código de autenticação de dois fatores", + "TwoFactorCode": "Código de autenticação de dois fatores", + "Verify": "Verificar", + "SetPassword": "Definir senha", + "SSOPasswordDescription": "Sua conta usa login externo. Defina uma senha através da confirmação por e-mail.", + "SendSetupLink": "Enviar link de configuração", + "SSOPasswordEmailSent": "Verifique seu e-mail para encontrar o link de configuração da senha.", + "SSONoEmailLinked": "Nenhum endereço de e-mail está vinculado à sua conta. Adicione um em Configurações da Conta → Gerenciar Identidades primeiro." } } diff --git a/plugins/login-assets/lang/ru.json b/plugins/login-assets/lang/ru.json index fb4335b7af..eaf7bcaf38 100644 --- a/plugins/login-assets/lang/ru.json +++ b/plugins/login-assets/lang/ru.json @@ -92,10 +92,14 @@ "SignUpAndJoin": "Зарегистрироваться и присоединиться", "CreateNewAccount": "Создать новый аккаунт", "SignedInAs": "Вы вошли как {name}", - "SetPassword": "Set a password", - "SSOPasswordDescription": "Your account uses external login. Set up a password via email confirmation.", - "SendSetupLink": "Send setup link", - "SSOPasswordEmailSent": "Check your email for a link to set your password.", - "SSONoEmailLinked": "No email address is linked to your account. Add one in Account Settings → Manage Identities first." + "TwoFactorAuth": "Двухфакторная аутентификация", + "EnterTwoFactorCode": "Введите код двухфакторной аутентификации", + "TwoFactorCode": "Код двухфакторной аутентификации", + "Verify": "Проверить", + "SetPassword": "Установить пароль", + "SSOPasswordDescription": "Ваша учетная запись использует внешний вход. Установите пароль через подтверждение по электронной почте.", + "SendSetupLink": "Отправить ссылку для настройки", + "SSOPasswordEmailSent": "Проверьте свою электронную почту, чтобы найти ссылку для установки пароля.", + "SSONoEmailLinked": "К вашей учетной записи не привязан адрес электронной почты. Сначала добавьте его в Настройки аккаунта → Управление идентификаторами." } } diff --git a/plugins/login-assets/lang/tr.json b/plugins/login-assets/lang/tr.json index 827683b752..5a5e3b25f1 100644 --- a/plugins/login-assets/lang/tr.json +++ b/plugins/login-assets/lang/tr.json @@ -92,10 +92,14 @@ "SignUpAndJoin": "Kayıt ol ve katıl", "CreateNewAccount": "Yeni hesap oluştur", "SignedInAs": "{name} olarak giriş yapıldı", - "SetPassword": "Set a password", - "SSOPasswordDescription": "Your account uses external login. Set up a password via email confirmation.", - "SendSetupLink": "Send setup link", - "SSOPasswordEmailSent": "Check your email for a link to set your password.", - "SSONoEmailLinked": "No email address is linked to your account. Add one in Account Settings → Manage Identities first." + "TwoFactorAuth": "İki faktörlü kimlik doğrulama", + "EnterTwoFactorCode": "İki faktörlü kimlik doğrulama kodunu girin", + "TwoFactorCode": "İki faktörlü kimlik doğrulama kodu", + "Verify": "Doğrula", + "SetPassword": "Şifreyi ayarla", + "SSOPasswordDescription": "Hesabınız harici girişi kullanıyor. E-posta onayı ile bir şifre belirleyin.", + "SendSetupLink": "Kurulum bağlantısı gönder", + "SSOPasswordEmailSent": "Şifrenizi belirlemek için bir bağlantı içeren e-postayı kontrol edin.", + "SSONoEmailLinked": "Hesabınıza bağlı bir e-posta adresi yok. Önce Hesap Ayarları → Kimlikleri Yönet bölümünden bir tane ekleyin." } } diff --git a/plugins/login-assets/lang/zh.json b/plugins/login-assets/lang/zh.json index dfd0becf22..8594899508 100644 --- a/plugins/login-assets/lang/zh.json +++ b/plugins/login-assets/lang/zh.json @@ -92,10 +92,14 @@ "SignUpAndJoin": "注册并加入", "CreateNewAccount": "创建新账户", "SignedInAs": "已以 {name} 身份登录", - "SetPassword": "Set a password", - "SSOPasswordDescription": "Your account uses external login. Set up a password via email confirmation.", - "SendSetupLink": "Send setup link", - "SSOPasswordEmailSent": "Check your email for a link to set your password.", - "SSONoEmailLinked": "No email address is linked to your account. Add one in Account Settings → Manage Identities first." + "TwoFactorAuth": "两步验证", + "EnterTwoFactorCode": "输入两步验证码", + "TwoFactorCode": "两步验证码", + "Verify": "验证", + "SetPassword": "设置密码", + "SSOPasswordDescription": "您的账户使用外部登录。请通过电子邮件确认设置密码。", + "SendSetupLink": "发送设置链接", + "SSOPasswordEmailSent": "请检查您的电子邮件,获取设置密码的链接。", + "SSONoEmailLinked": "您的账户没有关联的电子邮件地址。请先在账户设置 → 管理身份中添加。" } } diff --git a/plugins/login-resources/src/components/LoginApp.svelte b/plugins/login-resources/src/components/LoginApp.svelte index 9852a5af73..2af6f572bd 100644 --- a/plugins/login-resources/src/components/LoginApp.svelte +++ b/plugins/login-resources/src/components/LoginApp.svelte @@ -41,6 +41,7 @@ import PasswordRestore from './PasswordRestore.svelte' import SelectWorkspace from './SelectWorkspace.svelte' import SignupForm from './SignupForm.svelte' + import LoginTfaForm from './LoginTfaForm.svelte' import LoginIcon from './icons/LoginIcon.svelte' import { Pages, getAccount, pages } from '..' import login from '../plugin' @@ -60,6 +61,7 @@ const localLoginHidden = getMetadata(login.metadata.HideLocalLogin) ?? false const useOTP = getMetadata(presentation.metadata.MailUrl) != null && getMetadata(presentation.metadata.MailUrl) !== '' let navigateUrl: string | undefined + let tfaToken: string | undefined = undefined onDestroy(location.subscribe(updatePageLoc)) @@ -79,7 +81,8 @@ 'autoJoin', 'confirm', 'confirmationSend', - 'auth' + 'auth', + 'tfa' ] if (token === undefined ? !allowedUnauthPages.includes(page) : !pages.includes(page)) { const account = fetchMetadataLocalStorage(login.metadata.LastAccount) @@ -87,6 +90,7 @@ } navigateUrl = loc.query?.navigateUrl ?? undefined + tfaToken = loc.query?.token ?? undefined } async function chooseToken (): Promise { @@ -180,6 +184,8 @@ {:else if page === 'changePassword'} + {:else if page === 'tfa'} + (page = 'login')} /> {/if} diff --git a/plugins/login-resources/src/components/LoginTfaForm.svelte b/plugins/login-resources/src/components/LoginTfaForm.svelte new file mode 100644 index 0000000000..02b7eeb3f1 --- /dev/null +++ b/plugins/login-resources/src/components/LoginTfaForm.svelte @@ -0,0 +1,65 @@ + + + +
diff --git a/plugins/login-resources/src/utils.ts b/plugins/login-resources/src/utils.ts index 3c6c32c658..57abff5b84 100644 --- a/plugins/login-resources/src/utils.ts +++ b/plugins/login-resources/src/utils.ts @@ -1025,12 +1025,56 @@ export async function doValidateOtp ( } } +export async function verify2fa (code: string, token: string | undefined): Promise<[Status, LoginInfo | null]> { + if (token === undefined) { + return [new Status(Severity.ERROR, platform.status.Unauthorized, {}), null] + } + + try { + const loginInfo = await getAccountClient(token).verify2fa(code) + + Analytics.handleEvent('verify2fa', { ok: true }) + + return [OK, loginInfo] + } catch (err: any) { + Analytics.handleEvent('verify2fa', { ok: false }) + if (err instanceof PlatformError) { + await handleStatusError('Verify 2fa error', err.status) + + return [err.status, null] + } else { + console.error('Verify 2fa error', err) + Analytics.handleError(err) + + return [unknownError(err), null] + } + } +} + export async function doLoginNavigate ( result: LoginInfo | null, updateStatus: (status: Status) => void, navigateUrl?: string ): Promise { if (result != null) { + if (result.tfaRequired === true) { + const currentLoc = getCurrentLocation() + const loc = getCurrentLocation() + loc.path[1] = 'tfa' + loc.path.length = 2 + if (navigateUrl !== undefined || result.token != null) { + loc.query = { ...loc.query, navigateUrl: navigateUrl ?? null, token: result.token ?? null } + } + + if (loc.path.length === currentLoc.path.length && isSameSegments(currentLoc, loc, loc.path.length)) { + window.location.reload() + return + } + + navigate(loc) + return + } + if (result.token != null) { await logIn(result) } diff --git a/plugins/login/src/index.ts b/plugins/login/src/index.ts index f4b8331f8b..4182d93ed9 100644 --- a/plugins/login/src/index.ts +++ b/plugins/login/src/index.ts @@ -40,7 +40,8 @@ export const pages = [ 'confirmationSend', 'auth', 'login-password', - 'changePassword' + 'changePassword', + 'tfa' ] as const export type Pages = (typeof pages)[number] @@ -97,7 +98,11 @@ export default plugin(loginId, { PasswordExpiredDesc: '' as IntlString, Email: '' as IntlString, Password: '' as IntlString, - PasswordRepeat: '' as IntlString + PasswordRepeat: '' as IntlString, + TwoFactorAuth: '' as IntlString, + EnterTwoFactorCode: '' as IntlString, + TwoFactorCode: '' as IntlString, + Verify: '' as IntlString }, function: { SendInvite: '' as Resource<(email: string, role: AccountRole) => Promise>, diff --git a/plugins/setting-assets/lang/cs.json b/plugins/setting-assets/lang/cs.json index 3be383035a..5f86305ff5 100644 --- a/plugins/setting-assets/lang/cs.json +++ b/plugins/setting-assets/lang/cs.json @@ -217,6 +217,15 @@ "SelectUsers": "Vybrat uživatele", "ShowInTitle": "Zobrazit v názvu", "SpaceMembersOnly": "Pouze členové prostoru", - "Reset": "Resetovat" + "Reset": "Resetovat", + "Security": "Zabezpečení", + "TwoFactorAuth": "Dvoufaktorové ověřování", + "TwoFactorAuthDescription": "Dvoufaktorové ověřování přidává další vrstvu zabezpečení k vašemu účtu", + "EnableTwoFactorAuth": "Povolit dvoufaktorové ověřování", + "DisableTwoFactorAuth": "Zakázat dvoufaktorové ověřování", + "TwoFactorAuthEnabled": "Dvoufaktorové ověřování je povoleno", + "TwoFactorAuthDisabled": "Dvoufaktorové ověřování je zakázáno", + "ShowQRCode": "Zobrazit QR kód", + "EnterVerificationCode": "Zadejte ověřovací kód" } } diff --git a/plugins/setting-assets/lang/de.json b/plugins/setting-assets/lang/de.json index 1f7940a705..0a7e00ceb9 100644 --- a/plugins/setting-assets/lang/de.json +++ b/plugins/setting-assets/lang/de.json @@ -219,6 +219,15 @@ "SelectUsers": "Benutzer auswählen", "ShowInTitle": "Im Titel anzeigen", "SpaceMembersOnly": "Nur Bereichsmitglieder", - "Reset": "Zurücksetzen" + "Reset": "Zurücksetzen", + "Security": "Sicherheit", + "TwoFactorAuth": "Zweistufige Authentifizierung", + "TwoFactorAuthDescription": "Zweistufige Authentifizierung fügt eine zusätzliche Sicherheitsebene zu Ihrem Konto hinzu", + "EnableTwoFactorAuth": "Zweistufige Authentifizierung aktivieren", + "DisableTwoFactorAuth": "Zweistufige Authentifizierung deaktivieren", + "TwoFactorAuthEnabled": "Zweistufige Authentifizierung ist aktiviert", + "TwoFactorAuthDisabled": "Zweistufige Authentifizierung ist deaktiviert", + "ShowQRCode": "QR-Code anzeigen", + "EnterVerificationCode": "Verifizierungscode eingeben" } } diff --git a/plugins/setting-assets/lang/en.json b/plugins/setting-assets/lang/en.json index 0847e55f7a..f235562cbd 100644 --- a/plugins/setting-assets/lang/en.json +++ b/plugins/setting-assets/lang/en.json @@ -219,6 +219,15 @@ "ImportDocumentDescription": "Grants users ability to import documents into the workspace", "SelectUsers": "Select users", "ShowInTitle": "Show in title", - "SpaceMembersOnly": "Space members only" + "SpaceMembersOnly": "Space members only", + "Security": "Security", + "TwoFactorAuth": "Two-factor authentication", + "TwoFactorAuthDescription": "Two-factor authentication adds an extra layer of security to your account", + "EnableTwoFactorAuth": "Enable two-factor authentication", + "DisableTwoFactorAuth": "Disable two-factor authentication", + "TwoFactorAuthEnabled": "Two-factor authentication is enabled", + "TwoFactorAuthDisabled": "Two-factor authentication is disabled", + "ShowQRCode": "Show QR code", + "EnterVerificationCode": "Enter verification code" } } diff --git a/plugins/setting-assets/lang/es.json b/plugins/setting-assets/lang/es.json index 8afc019dd6..d2588bb0cc 100644 --- a/plugins/setting-assets/lang/es.json +++ b/plugins/setting-assets/lang/es.json @@ -210,6 +210,15 @@ "SelectUsers": "Seleccionar usuarios", "ShowInTitle": "Mostrar en el título", "SpaceMembersOnly": "Solo miembros del espacio", - "Reset": "Reiniciar" + "Reset": "Reiniciar", + "Security": "Seguridad", + "TwoFactorAuth": "Autenticación de dos factores", + "TwoFactorAuthDescription": "La autenticación de dos factores añade una capa adicional de seguridad a tu cuenta", + "EnableTwoFactorAuth": "Habilitar autenticación de dos factores", + "DisableTwoFactorAuth": "Deshabilitar autenticación de dos factores", + "TwoFactorAuthEnabled": "La autenticación de dos factores está habilitada", + "TwoFactorAuthDisabled": "La autenticación de dos factores está deshabilitada", + "ShowQRCode": "Mostrar código QR", + "EnterVerificationCode": "Introducir código de verificación" } } diff --git a/plugins/setting-assets/lang/fr.json b/plugins/setting-assets/lang/fr.json index 1a5f500790..544d315a63 100644 --- a/plugins/setting-assets/lang/fr.json +++ b/plugins/setting-assets/lang/fr.json @@ -219,6 +219,15 @@ "SelectUsers": "Sélectionner des utilisateurs", "ShowInTitle": "Afficher dans le titre", "SpaceMembersOnly": "Membres de l'espace uniquement", - "Reset": "Réinitialiser" + "Reset": "Réinitialiser", + "Security": "Sécurité", + "TwoFactorAuth": "Authentification à deux facteurs", + "TwoFactorAuthDescription": "L'authentification à deux facteurs ajoute une couche de sécurité supplémentaire à votre compte", + "EnableTwoFactorAuth": "Activer l'authentification à deux facteurs", + "DisableTwoFactorAuth": "Désactiver l'authentification à deux facteurs", + "TwoFactorAuthEnabled": "L'authentification à deux facteurs est activée", + "TwoFactorAuthDisabled": "L'authentification à deux facteurs est désactivée", + "ShowQRCode": "Afficher le code QR", + "EnterVerificationCode": "Entrer le code de vérification" } } diff --git a/plugins/setting-assets/lang/it.json b/plugins/setting-assets/lang/it.json index c84180bad6..a24b761625 100644 --- a/plugins/setting-assets/lang/it.json +++ b/plugins/setting-assets/lang/it.json @@ -219,6 +219,15 @@ "SelectUsers": "Seleziona utenti", "ShowInTitle": "Mostra nel titolo", "SpaceMembersOnly": "Solo membri dello spazio", - "Reset": "Reset" + "Reset": "Reset", + "Security": "Sicurezza", + "TwoFactorAuth": "Autenticazione a due fattori", + "TwoFactorAuthDescription": "L'autenticazione a due fattori aggiunge un ulteriore livello di sicurezza al tuo account", + "EnableTwoFactorAuth": "Abilita autenticazione a due fattori", + "DisableTwoFactorAuth": "Disabilita autenticazione a due fattori", + "TwoFactorAuthEnabled": "L'autenticazione a due fattori è abilitata", + "TwoFactorAuthDisabled": "L'autenticazione a due fattori è disabilitata", + "ShowQRCode": "Mostra codice QR", + "EnterVerificationCode": "Inserisci codice di verifica" } } diff --git a/plugins/setting-assets/lang/ja.json b/plugins/setting-assets/lang/ja.json index e93be6da77..bd4c6c83af 100644 --- a/plugins/setting-assets/lang/ja.json +++ b/plugins/setting-assets/lang/ja.json @@ -219,6 +219,15 @@ "SelectUsers": "ユーザーを選択", "ShowInTitle": "タイトルに表示", "SpaceMembersOnly": "スペースメンバーのみ", - "Reset": "リセット" + "Reset": "リセット", + "Security": "セキュリティ", + "TwoFactorAuth": "二要素認証", + "TwoFactorAuthDescription": "二要素認証はアカウントにセキュリティの追加レイヤーを追加します", + "EnableTwoFactorAuth": "二要素認証を有効にする", + "DisableTwoFactorAuth": "二要素認証を無効にする", + "TwoFactorAuthEnabled": "二要素認証は有効です", + "TwoFactorAuthDisabled": "二要素認証は無効です", + "ShowQRCode": "QRコードを表示", + "EnterVerificationCode": "確認コードを入力" } } diff --git a/plugins/setting-assets/lang/pt-br.json b/plugins/setting-assets/lang/pt-br.json index 5b3a235f81..05b1bc7ada 100644 --- a/plugins/setting-assets/lang/pt-br.json +++ b/plugins/setting-assets/lang/pt-br.json @@ -210,6 +210,15 @@ "SelectUsers": "Selecionar usuários", "ShowInTitle": "Mostrar no título", "SpaceMembersOnly": "Apenas membros do espaço", - "Reset": "Reiniciar" + "Reset": "Reiniciar", + "Security": "Segurança", + "TwoFactorAuth": "Autenticação de dois fatores", + "TwoFactorAuthDescription": "A autenticação de dois fatores adiciona uma camada extra de segurança à sua conta", + "EnableTwoFactorAuth": "Ativar autenticação de dois fatores", + "DisableTwoFactorAuth": "Desativar autenticação de dois fatores", + "TwoFactorAuthEnabled": "Autenticação de dois fatores está ativada", + "TwoFactorAuthDisabled": "Autenticação de dois fatores está desativada", + "ShowQRCode": "Mostrar código QR", + "EnterVerificationCode": "Inserir código de verificação" } } diff --git a/plugins/setting-assets/lang/pt.json b/plugins/setting-assets/lang/pt.json index 98426776ea..fb80fd05d8 100644 --- a/plugins/setting-assets/lang/pt.json +++ b/plugins/setting-assets/lang/pt.json @@ -210,6 +210,15 @@ "SelectUsers": "Selecionar usuários", "ShowInTitle": "Mostrar no título", "SpaceMembersOnly": "Apenas membros do espaço", - "Reset": "Reiniciar" + "Reset": "Reiniciar", + "Security": "Segurança", + "TwoFactorAuth": "Autenticação de dois fatores", + "TwoFactorAuthDescription": "A autenticação de dois fatores adiciona uma camada extra de segurança à sua conta", + "EnableTwoFactorAuth": "Ativar autenticação de dois fatores", + "DisableTwoFactorAuth": "Desativar autenticação de dois fatores", + "TwoFactorAuthEnabled": "Autenticação de dois fatores está ativada", + "TwoFactorAuthDisabled": "Autenticação de dois fatores está desativada", + "ShowQRCode": "Mostrar código QR", + "EnterVerificationCode": "Inserir código de verificação" } } diff --git a/plugins/setting-assets/lang/ru.json b/plugins/setting-assets/lang/ru.json index cc0abb4926..1af522701c 100644 --- a/plugins/setting-assets/lang/ru.json +++ b/plugins/setting-assets/lang/ru.json @@ -219,6 +219,15 @@ "ImportDocumentDescription": "Предоставляет пользователям возможность импортировать документы в рабочее пространство", "SelectUsers": "Выбрать пользователей", "ShowInTitle": "Показывать в заголовке", - "SpaceMembersOnly": "Только участники пространства" + "SpaceMembersOnly": "Только участники пространства", + "Security": "Безопасность", + "TwoFactorAuth": "Двухфакторная аутентификация", + "TwoFactorAuthDescription": "Двухфакторная аутентификация добавляет дополнительный уровень безопасности к вашей учетной записи", + "EnableTwoFactorAuth": "Включить двухфакторную аутентификацию", + "DisableTwoFactorAuth": "Отключить двухфакторную аутентификацию", + "TwoFactorAuthEnabled": "Двухфакторная аутентификация включена", + "TwoFactorAuthDisabled": "Двухфакторная аутентификация отключена", + "ShowQRCode": "Показать QR-код", + "EnterVerificationCode": "Введите код подтверждения" } } diff --git a/plugins/setting-assets/lang/tr.json b/plugins/setting-assets/lang/tr.json index 7da019578a..9b92fe4f11 100644 --- a/plugins/setting-assets/lang/tr.json +++ b/plugins/setting-assets/lang/tr.json @@ -219,6 +219,15 @@ "SelectUsers": "Kullanıcıları seç", "ShowInTitle": "Başlıkta göster", "SpaceMembersOnly": "Yalnızca alan üyeleri", - "Reset": "Sıfırla" + "Reset": "Sıfırla", + "Security": "Güvenlik", + "TwoFactorAuth": "İki faktörlü kimlik doğrulama", + "TwoFactorAuthDescription": "İki faktörlü kimlik doğrulama hesabınıza ek bir güvenlik katmanı ekler", + "EnableTwoFactorAuth": "İki faktörlü kimlik doğrulamayı etkinleştir", + "DisableTwoFactorAuth": "İki faktörlü kimlik doğrulamayı devre dışı bırak", + "TwoFactorAuthEnabled": "İki faktörlü kimlik doğrulama etkin", + "TwoFactorAuthDisabled": "İki faktörlü kimlik doğrulama devre dışı", + "ShowQRCode": "QR kodu göster", + "EnterVerificationCode": "Doğrulama kodunu gir" } } diff --git a/plugins/setting-assets/lang/zh.json b/plugins/setting-assets/lang/zh.json index 423257c2c0..1146fc9f78 100644 --- a/plugins/setting-assets/lang/zh.json +++ b/plugins/setting-assets/lang/zh.json @@ -219,6 +219,15 @@ "SelectUsers": "选择用户", "ShowInTitle": "在标题中显示", "SpaceMembersOnly": "仅限空间成员", - "Reset": "重置" + "Reset": "重置", + "Security": "安全", + "TwoFactorAuth": "双因素认证", + "TwoFactorAuthDescription": "双因素认证为您的帐户增加额外的安全层", + "EnableTwoFactorAuth": "启用双因素认证", + "DisableTwoFactorAuth": "禁用双因素认证", + "TwoFactorAuthEnabled": "双因素认证已启用", + "TwoFactorAuthDisabled": "双因素认证已禁用", + "ShowQRCode": "显示QR码", + "EnterVerificationCode": "输入验证码" } } diff --git a/plugins/setting-resources/package.json b/plugins/setting-resources/package.json index 5fb70c7335..909ab1e157 100644 --- a/plugins/setting-resources/package.json +++ b/plugins/setting-resources/package.json @@ -37,7 +37,8 @@ "jest": "^29.7.0", "ts-jest": "^29.1.1", "@types/jest": "^29.5.5", - "svelte-eslint-parser": "^0.33.1" + "svelte-eslint-parser": "^0.33.1", + "@types/qrcode": "^1.5.5" }, "dependencies": { "@hcengineering/platform": "workspace:^0.7.19", @@ -66,6 +67,7 @@ "@hcengineering/chat": "workspace:^0.7.0", "@hcengineering/integration-client": "workspace:^0.7.0", "@hcengineering/rank": "workspace:^0.7.17", - "@hcengineering/rating": "workspace:^0.7.0" + "@hcengineering/rating": "workspace:^0.7.0", + "qrcode": "^1.5.4" } } diff --git a/plugins/setting-resources/src/components/TwoFactorSettings.svelte b/plugins/setting-resources/src/components/TwoFactorSettings.svelte new file mode 100644 index 0000000000..a0678cba18 --- /dev/null +++ b/plugins/setting-resources/src/components/TwoFactorSettings.svelte @@ -0,0 +1,152 @@ + + + +
+
+ +
+ +
+
+
+ + {#if showSetup} +
+ {#if !tfaEnabled} +
+
+ 2FA QR Code +
+
+ {secret} +
+
+ {/if} + +
+
+
+ {/if} +
+
diff --git a/plugins/setting-resources/src/index.ts b/plugins/setting-resources/src/index.ts index 30a09f6a3f..1f3709f4d7 100644 --- a/plugins/setting-resources/src/index.ts +++ b/plugins/setting-resources/src/index.ts @@ -30,6 +30,7 @@ import Password from './components/Password.svelte' import Privacy from './components/Privacy.svelte' import Profile from './components/Profile.svelte' import Settings from './components/Settings.svelte' +import TwoFactorSettings from './components/TwoFactorSettings.svelte' import { Analytics } from '@hcengineering/analytics' import ClassAttributes from './components/ClassAttributes.svelte' @@ -167,7 +168,8 @@ export default async (): Promise => ({ AddSocialId, AddEmailSocialId, EmployeeRefEditor, - UserRoleSelect + UserRoleSelect, + TwoFactorSettings }, actionImpl: { DeleteMixin diff --git a/plugins/setting/src/index.ts b/plugins/setting/src/index.ts index b9836b6995..a22f122c0e 100644 --- a/plugins/setting/src/index.ts +++ b/plugins/setting/src/index.ts @@ -199,7 +199,8 @@ export default plugin(settingId, { Export: '' as Ref, OfficeSettings: '' as Ref, DisablePermissionsConfiguration: '' as Ref, - Mailboxes: '' as Ref + Mailboxes: '' as Ref, + Security: '' as Ref }, mixin: { Editable: '' as Ref>, @@ -244,7 +245,8 @@ export default plugin(settingId, { Mailboxes: '' as AnyComponent, AddEmailSocialId: '' as AnyComponent, OfficeSettings: '' as AnyComponent, - UserRoleSelect: '' as AnyComponent + UserRoleSelect: '' as AnyComponent, + TwoFactorSettings: '' as AnyComponent }, string: { Settings: '' as IntlString, @@ -316,6 +318,15 @@ export default plugin(settingId, { MailboxErrorMailboxCountLimit: '' as IntlString, DeleteMailbox: '' as IntlString, MailboxDeleteConfirmation: '' as IntlString, + Security: '' as IntlString, + TwoFactorAuth: '' as IntlString, + TwoFactorAuthDescription: '' as IntlString, + EnableTwoFactorAuth: '' as IntlString, + DisableTwoFactorAuth: '' as IntlString, + TwoFactorAuthEnabled: '' as IntlString, + TwoFactorAuthDisabled: '' as IntlString, + ShowQRCode: '' as IntlString, + EnterVerificationCode: '' as IntlString, IntegrationFailed: '' as IntlString, IntegrationError: '' as IntlString, EmailIsUsed: '' as IntlString, diff --git a/plugins/workbench-resources/src/connect.ts b/plugins/workbench-resources/src/connect.ts index 0b6b13a49b..3c2d38b13a 100644 --- a/plugins/workbench-resources/src/connect.ts +++ b/plugins/workbench-resources/src/connect.ts @@ -127,6 +127,8 @@ export async function connect (title: string): Promise { break } + console.log('workspaceLoginInfo', workspaceLoginInfo) + const token = workspaceLoginInfo.token setMetadata(presentation.metadata.Token, workspaceLoginInfo.token) diff --git a/server/account/package.json b/server/account/package.json index 25a5ffaa0e..7ea94768ef 100644 --- a/server/account/package.json +++ b/server/account/package.json @@ -47,6 +47,7 @@ "@hcengineering/analytics": "workspace:^0.7.17", "@hcengineering/server-storage": "workspace:^0.7.16", "@hcengineering/server-core": "workspace:^0.7.18", + "otplib": "^12.0.1", "@hcengineering/server-pipeline": "workspace:^0.7.0" } } diff --git a/server/account/src/__tests__/operations.test.ts b/server/account/src/__tests__/operations.test.ts index 23957ac14a..959e3470cd 100644 --- a/server/account/src/__tests__/operations.test.ts +++ b/server/account/src/__tests__/operations.test.ts @@ -1400,7 +1400,8 @@ describe('account operations', () => { account: mockAccountId, token: expect.any(String), name: 'John Doe', - socialId: 'social-id' + socialId: 'social-id', + tfaRequired: false }) }) @@ -1439,7 +1440,8 @@ describe('account operations', () => { account: mockAccountId, token: undefined, name: 'John Doe', - socialId: 'social-id' + socialId: 'social-id', + tfaRequired: false }) }) @@ -1984,7 +1986,8 @@ describe('account operations', () => { account: mockPersonId, name: 'John Doe', socialId: mockSocialId._id, - token: expect.any(String) + token: expect.any(String), + tfaRequired: false }) expect(mockDb.otp.deleteMany).toHaveBeenCalledWith({ socialId: mockSocialId._id }) @@ -2030,7 +2033,8 @@ describe('account operations', () => { account: mockPersonId, name: 'John Doe', socialId: mockSocialId._id, - token: expect.any(String) + token: expect.any(String), + tfaRequired: false }) expect(mockDb.otp.deleteMany).toHaveBeenCalledWith({ socialId: mockSocialId._id }) @@ -2071,7 +2075,8 @@ describe('account operations', () => { account: callerAccountId, name: 'John Doe', socialId: mockSocialId._id, - token: expect.any(String) + token: undefined, + tfaRequired: false }) expect(mockDb.otp.deleteMany).toHaveBeenCalledWith({ socialId: mockSocialId._id }) @@ -2126,7 +2131,8 @@ describe('account operations', () => { account: callerAccountId, name: 'John Doe', socialId: mockSocialId._id, - token: expect.any(String) + token: undefined, + tfaRequired: false }) expect(mockDb.otp.deleteMany).toHaveBeenCalledWith({ socialId: mockSocialId._id }) @@ -2173,7 +2179,8 @@ describe('account operations', () => { account: callerAccountId, name: 'John Doe', socialId: mockSocialId._id, - token: expect.any(String) + token: undefined, + tfaRequired: false }) expect(mockDb.otp.deleteMany).toHaveBeenCalledWith({ socialId: mockSocialId._id }) @@ -2476,7 +2483,8 @@ describe('account operations', () => { account: mockAccountId, name: 'John Doe', token: expect.any(String), - socialId: mockSocialId._id + socialId: mockSocialId._id, + tfaRequired: false }) expect(utils.setPassword).toHaveBeenCalledWith(mockCtx, mockDb, mockBranding, mockAccountId, mockNewPassword) diff --git a/server/account/src/__tests__/postgres.test.ts b/server/account/src/__tests__/postgres.test.ts index 5b9e434c11..863ceb9baa 100644 --- a/server/account/src/__tests__/postgres.test.ts +++ b/server/account/src/__tests__/postgres.test.ts @@ -337,6 +337,7 @@ describe('AccountPostgresDbCollection', () => { a.automatic, a.max_workspaces, a.failed_login_attempts, + a.tfa_secret, p.hash, p.salt FROM global_account.account as a diff --git a/server/account/src/collections/postgres/migrations.ts b/server/account/src/collections/postgres/migrations.ts index c9bce8442c..adaef2de9c 100644 --- a/server/account/src/collections/postgres/migrations.ts +++ b/server/account/src/collections/postgres/migrations.ts @@ -81,7 +81,8 @@ export function getMigrations (ns: string, flavor: DBFlavor): [string, string][] getV21Migration(ns, flavor), getV22Migration(ns, flavor), getV23Migration(ns, flavor), - getV24Migration(ns, flavor) + getV24Migration(ns, flavor), + getV25Migration(ns, flavor) ] } @@ -782,3 +783,14 @@ function getV24Migration (ns: string, flavor: DBFlavor): [string, string] { ` ] } + +function getV25Migration (ns: string, flavor: DBFlavor): [string, string] { + const types = dbTypes[flavor] + return [ + 'account_db_v25_add_2fa_to_account', + ` + ALTER TABLE ${ns}.account + ADD COLUMN IF NOT EXISTS tfa_secret ${types.string}; + ` + ] +} diff --git a/server/account/src/collections/postgres/postgres.ts b/server/account/src/collections/postgres/postgres.ts index 53bc5f98ba..4ab5aea2b1 100644 --- a/server/account/src/collections/postgres/postgres.ts +++ b/server/account/src/collections/postgres/postgres.ts @@ -453,6 +453,7 @@ export class AccountPostgresDbCollection a.automatic, a.max_workspaces, a.failed_login_attempts, + a.tfa_secret, p.hash, p.salt FROM ${this.getTableName()} as a diff --git a/server/account/src/operations.ts b/server/account/src/operations.ts index be19c3a3f5..a1869b41c9 100644 --- a/server/account/src/operations.ts +++ b/server/account/src/operations.ts @@ -123,9 +123,14 @@ import { recordFailedLoginAttempt, resetFailedLoginAttempts, updatePasswordAgingRule, - checkPasswordAging + checkPasswordAging, + generateTotpSecret, + verifyTotpCode, + getTotpUrl } from './utils' +const NIL_UUID = '00000000-0000-0000-0000-000000000000' as AccountUuid + // Note: it is IMPORTANT to always destructure params passed here to avoid sending extra params // to the database layer when searching/inserting as they may contain SQL injection // !!! NEVER PASS "params" DIRECTLY in any DB functions !!! @@ -229,9 +234,16 @@ export async function login ( return { account: existingAccount.uuid, - token: isConfirmed ? generateToken(existingAccount.uuid, undefined, extraToken) : undefined, + token: isConfirmed + ? generateToken( + existingAccount.tfaSecret != null ? NIL_UUID : existingAccount.uuid, + undefined, + existingAccount.tfaSecret != null ? { ...extraToken, tfaAccount: existingAccount.uuid } : extraToken + ) + : undefined, name: getPersonName(person), - socialId: emailSocialId._id + socialId: emailSocialId._id, + tfaRequired: isConfirmed && existingAccount.tfaSecret != null } } catch (err: any) { Analytics.handleError(err) @@ -508,15 +520,26 @@ export async function validateOtp ( await resetFailedLoginAttempts(db, emailSocialId.personUuid as AccountUuid) + const isConfirmed = emailSocialId.verifiedOn != null || action !== 'verify' + const extraToken: Record = isAdminEmail(normalizedEmail) ? { admin: 'true', authMethod: 'otp' } : { authMethod: 'otp' } + const _token = isConfirmed + ? generateToken( + targetAccount?.tfaSecret != null ? NIL_UUID : emailSocialId.personUuid, + undefined, + targetAccount?.tfaSecret != null ? { ...extraToken, tfaAccount: emailSocialId.personUuid } : extraToken + ) + : undefined + return { account: emailSocialId.personUuid as AccountUuid, name: getPersonName(person), socialId: emailSocialId._id, - token: generateToken(emailSocialId.personUuid, undefined, extraToken) + token: _token, + tfaRequired: targetAccount?.tfaSecret != null } } catch (err: any) { Analytics.handleError(err) @@ -1694,6 +1717,112 @@ export async function deleteWorkspace ( ) } +export async function generate2faSecret ( + ctx: MeasureContext, + db: AccountDB, + branding: Branding | null, + token: string +): Promise<{ secret: string, url: string }> { + const { account: accountUuid } = decodeTokenVerbose(ctx, token) + const account = await getAccount(db, accountUuid) + if (account == null) { + throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, { account: accountUuid })) + } + + const emailSocialId = await db.socialId.findOne({ personUuid: accountUuid, type: SocialIdType.EMAIL }) + if (emailSocialId == null) { + throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {})) + } + + const secret = generateTotpSecret() + const app = branding?.title ?? getMetadata(accountPlugin.metadata.ProductName) ?? 'Huly' + const url = getTotpUrl(emailSocialId.value, app, secret) + + return { secret, url } +} + +export async function enable2fa ( + ctx: MeasureContext, + db: AccountDB, + branding: Branding | null, + token: string, + params: { secret: string, code: string } +): Promise { + const { secret, code } = params + const { account: accountUuid } = decodeTokenVerbose(ctx, token) + + if (!verifyTotpCode(secret, code)) { + throw new PlatformError(new Status(Severity.ERROR, platform.status.InvalidOtp, {})) + } + + await db.account.update({ uuid: accountUuid }, { tfaSecret: secret }) + ctx.info('2FA enabled', { accountUuid }) +} + +export async function disable2fa ( + ctx: MeasureContext, + db: AccountDB, + branding: Branding | null, + token: string, + params: { code: string } +): Promise { + const { code } = params + const { account: accountUuid } = decodeTokenVerbose(ctx, token) + + const account = await getAccount(db, accountUuid) + if (account?.tfaSecret == null) { + throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {})) + } + + if (!verifyTotpCode(account.tfaSecret, code)) { + throw new PlatformError(new Status(Severity.ERROR, platform.status.InvalidOtp, {})) + } + + await db.account.update({ uuid: accountUuid }, { tfaSecret: undefined }) + ctx.info('2FA disabled', { accountUuid }) +} + +export async function verify2fa ( + ctx: MeasureContext, + db: AccountDB, + branding: Branding | null, + token: string, + params: { code: string } +): Promise { + const { code } = params + const decoded = decodeTokenVerbose(ctx, token) + const accountUuid = + decoded.account === NIL_UUID || decoded.account == null + ? (decoded.extra?.tfaAccount as AccountUuid) + : decoded.account + const extra = decoded.extra + + const account = await getAccount(db, accountUuid) + if (account?.tfaSecret == null) { + throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {})) + } + + if (!verifyTotpCode(account.tfaSecret, code)) { + throw new PlatformError(new Status(Severity.ERROR, platform.status.InvalidOtp, {})) + } + + const person = await db.person.findOne({ uuid: accountUuid }) + if (person == null) { + throw new PlatformError(new Status(Severity.ERROR, platform.status.InternalServerError, {})) + } + + const socialId = await db.socialId.findOne({ personUuid: accountUuid, verifiedOn: { $gt: 0 } }) + + const { tfaAccount, ...filteredExtra } = extra ?? {} + + return { + account: accountUuid, + token: generateToken(accountUuid, undefined, filteredExtra), + name: getPersonName(person), + socialId: socialId?._id + } +} + /* =================================== */ /* ==========READ OPERATIONS========== */ /* =================================== */ @@ -2274,7 +2403,7 @@ export async function getAccountInfo ( if (account === undefined || account === null) { throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, {})) } - return { timezone: account?.timezone, locale: account?.locale } + return { timezone: account?.timezone, locale: account?.locale, tfaEnabled: account?.tfaSecret != null } } export async function ensurePerson ( @@ -3089,6 +3218,10 @@ export type AccountMethods = | 'changeUsername' | 'updateWorkspaceName' | 'deleteWorkspace' + | 'generate2faSecret' + | 'enable2fa' + | 'disable2fa' + | 'verify2fa' | 'getRegionInfo' | 'getUserWorkspaces' | 'getWorkspaceInfo' @@ -3167,6 +3300,10 @@ export function getMethods (hasSignUp: boolean = true): Partial