mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-17 18:05:42 +02:00
Two-Factor Authentication (2FA) (#10658)
* Two-Factor Authentication (2FA) Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Fix test Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Fix test Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Fix tests Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * More test fixes Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Fix tests Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> --------- Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Generated
+145
@@ -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
|
||||
|
||||
@@ -254,8 +254,14 @@ export interface AccountClient {
|
||||
getWorkspacePermissions: (params: { accountId: AccountUuid, permission: string }) => Promise<WorkspaceUuid[]>
|
||||
getWorkspaceUsersWithPermission: (params: { permission: string }) => Promise<AccountUuid[]>
|
||||
|
||||
verify2fa: (code: string) => Promise<LoginInfo>
|
||||
|
||||
setCookie: () => Promise<void>
|
||||
deleteCookie: () => Promise<void>
|
||||
|
||||
generate2faSecret: () => Promise<{ secret: string, url: string }>
|
||||
enable2fa: (secret: string, code: string) => Promise<void>
|
||||
disable2fa: (code: string) => Promise<void>
|
||||
}
|
||||
|
||||
/** @public */
|
||||
@@ -1335,6 +1341,42 @@ class AccountClientImpl implements AccountClient {
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
async verify2fa (code: string): Promise<LoginInfo> {
|
||||
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<void> {
|
||||
const request = {
|
||||
method: 'enable2fa' as const,
|
||||
params: { secret, code }
|
||||
}
|
||||
|
||||
await this.rpc(request)
|
||||
}
|
||||
|
||||
async disable2fa (code: string): Promise<void> {
|
||||
const request = {
|
||||
method: 'disable2fa' as const,
|
||||
params: { code }
|
||||
}
|
||||
|
||||
await this.rpc(request)
|
||||
}
|
||||
}
|
||||
|
||||
function withRetry<T, F extends (...args: any[]) => Promise<T>> (
|
||||
|
||||
@@ -19,6 +19,8 @@ export interface LoginInfo {
|
||||
name?: string
|
||||
socialId?: PersonId
|
||||
token?: string
|
||||
tfaRequired?: boolean
|
||||
extra?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface EndpointInfo {
|
||||
|
||||
@@ -960,6 +960,7 @@ export interface SocialId {
|
||||
export interface AccountInfo {
|
||||
timezone?: string
|
||||
locale?: string
|
||||
tfaEnabled?: boolean
|
||||
}
|
||||
|
||||
export type SocialKey = Pick<SocialId, 'type' | 'value'>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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の管理 で追加してください。"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "К вашей учетной записи не привязан адрес электронной почты. Сначала добавьте его в Настройки аккаунта → Управление идентификаторами."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "您的账户没有关联的电子邮件地址。请先在账户设置 → 管理身份中添加。"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
@@ -180,6 +184,8 @@
|
||||
<Auth />
|
||||
{:else if page === 'changePassword'}
|
||||
<ChangePassword />
|
||||
{:else if page === 'tfa'}
|
||||
<LoginTfaForm {navigateUrl} token={tfaToken} on:back={() => (page = 'login')} />
|
||||
{/if}
|
||||
</div>
|
||||
</Scroller>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<!--
|
||||
// Copyright © 2026 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { OK } from '@hcengineering/platform'
|
||||
import { doLoginNavigate, verify2fa } from '../utils'
|
||||
import login from '../plugin'
|
||||
import Form from './Form.svelte'
|
||||
|
||||
export let navigateUrl: string | undefined = undefined
|
||||
export let token: string | undefined = undefined
|
||||
|
||||
const object = {
|
||||
code: ''
|
||||
}
|
||||
|
||||
let status = OK
|
||||
let isLoading = false
|
||||
|
||||
$: fields = [
|
||||
{
|
||||
id: 'two-factor-code',
|
||||
name: 'code',
|
||||
i18n: login.string.TwoFactorCode,
|
||||
rules: [
|
||||
{
|
||||
rule: (v: string) => v.length === 6,
|
||||
notMatch: false,
|
||||
ruleDescr: login.status.IncorrectValue,
|
||||
ruleDescrParams: { field: 'code' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const action = {
|
||||
i18n: login.string.Verify,
|
||||
func: async () => {
|
||||
isLoading = true
|
||||
try {
|
||||
const [resStatus, loginInfo] = await verify2fa(object.code, token)
|
||||
status = resStatus
|
||||
|
||||
if (resStatus === OK && loginInfo != null) {
|
||||
await doLoginNavigate(loginInfo, (s) => (status = s), navigateUrl)
|
||||
}
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form caption={login.string.TwoFactorAuth} bind:status {fields} {object} {action} {isLoading} ignoreInitialValidation />
|
||||
@@ -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<void> {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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<void>>,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,6 +219,15 @@
|
||||
"SelectUsers": "ユーザーを選択",
|
||||
"ShowInTitle": "タイトルに表示",
|
||||
"SpaceMembersOnly": "スペースメンバーのみ",
|
||||
"Reset": "リセット"
|
||||
"Reset": "リセット",
|
||||
"Security": "セキュリティ",
|
||||
"TwoFactorAuth": "二要素認証",
|
||||
"TwoFactorAuthDescription": "二要素認証はアカウントにセキュリティの追加レイヤーを追加します",
|
||||
"EnableTwoFactorAuth": "二要素認証を有効にする",
|
||||
"DisableTwoFactorAuth": "二要素認証を無効にする",
|
||||
"TwoFactorAuthEnabled": "二要素認証は有効です",
|
||||
"TwoFactorAuthDisabled": "二要素認証は無効です",
|
||||
"ShowQRCode": "QRコードを表示",
|
||||
"EnterVerificationCode": "確認コードを入力"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,6 +219,15 @@
|
||||
"ImportDocumentDescription": "Предоставляет пользователям возможность импортировать документы в рабочее пространство",
|
||||
"SelectUsers": "Выбрать пользователей",
|
||||
"ShowInTitle": "Показывать в заголовке",
|
||||
"SpaceMembersOnly": "Только участники пространства"
|
||||
"SpaceMembersOnly": "Только участники пространства",
|
||||
"Security": "Безопасность",
|
||||
"TwoFactorAuth": "Двухфакторная аутентификация",
|
||||
"TwoFactorAuthDescription": "Двухфакторная аутентификация добавляет дополнительный уровень безопасности к вашей учетной записи",
|
||||
"EnableTwoFactorAuth": "Включить двухфакторную аутентификацию",
|
||||
"DisableTwoFactorAuth": "Отключить двухфакторную аутентификацию",
|
||||
"TwoFactorAuthEnabled": "Двухфакторная аутентификация включена",
|
||||
"TwoFactorAuthDisabled": "Двухфакторная аутентификация отключена",
|
||||
"ShowQRCode": "Показать QR-код",
|
||||
"EnterVerificationCode": "Введите код подтверждения"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,6 +219,15 @@
|
||||
"SelectUsers": "选择用户",
|
||||
"ShowInTitle": "在标题中显示",
|
||||
"SpaceMembersOnly": "仅限空间成员",
|
||||
"Reset": "重置"
|
||||
"Reset": "重置",
|
||||
"Security": "安全",
|
||||
"TwoFactorAuth": "双因素认证",
|
||||
"TwoFactorAuthDescription": "双因素认证为您的帐户增加额外的安全层",
|
||||
"EnableTwoFactorAuth": "启用双因素认证",
|
||||
"DisableTwoFactorAuth": "禁用双因素认证",
|
||||
"TwoFactorAuthEnabled": "双因素认证已启用",
|
||||
"TwoFactorAuthDisabled": "双因素认证已禁用",
|
||||
"ShowQRCode": "显示QR码",
|
||||
"EnterVerificationCode": "输入验证码"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
<!--
|
||||
// Copyright © 2026 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import QRCode from 'qrcode'
|
||||
import { getCurrentAccount } from '@hcengineering/core'
|
||||
import { OK, unknownError, getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import setting from '@hcengineering/setting'
|
||||
import { Breadcrumb, Button, EditBox, Header, Label, Spinner, Status } from '@hcengineering/ui'
|
||||
import { getAccountClient } from '../utils'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
|
||||
let tfaEnabled: boolean | undefined = undefined
|
||||
let showSetup = false
|
||||
let secret = ''
|
||||
let otpauthUrl = ''
|
||||
let code = ''
|
||||
let status = OK
|
||||
let isLoading = false
|
||||
|
||||
const acc = getCurrentAccount()
|
||||
|
||||
getAccountClient()
|
||||
.getAccountInfo(acc.uuid)
|
||||
.then((account) => {
|
||||
tfaEnabled = account.tfaEnabled
|
||||
})
|
||||
|
||||
async function toggle2fa () {
|
||||
if (tfaEnabled) {
|
||||
// Logic for disabling (needs code)
|
||||
showSetup = true
|
||||
} else {
|
||||
isLoading = true
|
||||
try {
|
||||
const result = await getAccountClient().generate2faSecret()
|
||||
secret = result.secret
|
||||
otpauthUrl = result.url
|
||||
showSetup = true
|
||||
} catch (err: any) {
|
||||
status = err.status ?? unknownError(err)
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyAndEnable () {
|
||||
isLoading = true
|
||||
try {
|
||||
await getAccountClient().enable2fa(secret, code)
|
||||
tfaEnabled = true
|
||||
showSetup = false
|
||||
code = ''
|
||||
status = OK
|
||||
} catch (err: any) {
|
||||
status = err.status ?? unknownError(err)
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyAndDisable () {
|
||||
isLoading = true
|
||||
try {
|
||||
await getAccountClient().disable2fa(code)
|
||||
tfaEnabled = false
|
||||
showSetup = false
|
||||
code = ''
|
||||
status = OK
|
||||
} catch (err: any) {
|
||||
status = err.status ?? unknownError(err)
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
let qrCodeUrl = ''
|
||||
|
||||
$: if (otpauthUrl) {
|
||||
QRCode.toDataURL(otpauthUrl, { margin: 1, width: 200 }).then((url) => {
|
||||
qrCodeUrl = url
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="hulyComponent">
|
||||
<Header adaptive={'disabled'}>
|
||||
<Breadcrumb icon={setting.icon.Password} label={setting.string.TwoFactorAuth} size="large" isCurrent />
|
||||
</Header>
|
||||
|
||||
<div class="flex-col p-6 gap-8 max-w-2xl">
|
||||
<div class="flex flex-between">
|
||||
<Label label={setting.string.TwoFactorAuthDescription} />
|
||||
{#if tfaEnabled === undefined}
|
||||
<Spinner />
|
||||
{:else}
|
||||
<Button
|
||||
label={tfaEnabled ? setting.string.DisableTwoFactorAuth : setting.string.EnableTwoFactorAuth}
|
||||
kind={tfaEnabled ? 'negative' : 'primary'}
|
||||
on:click={toggle2fa}
|
||||
disabled={isLoading || showSetup}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if showSetup}
|
||||
<div class="flex-col flex-gap-6 mt-6">
|
||||
{#if !tfaEnabled}
|
||||
<div class="flex-col items-center flex-gap-1">
|
||||
<div style="width: 200px; height: 200px">
|
||||
<img src={qrCodeUrl} alt="2FA QR Code" width="200" height="200" />
|
||||
</div>
|
||||
<div class="font-mono break-all">
|
||||
{secret}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex mt-4 items-center flex-gap-4">
|
||||
<Label label={setting.string.EnterVerificationCode} />:
|
||||
<EditBox
|
||||
bind:value={code}
|
||||
kind={'default-large'}
|
||||
maxWidth={'80px'}
|
||||
placeholder={getEmbeddedLabel('000000')}
|
||||
autoFocus
|
||||
/>
|
||||
<Button
|
||||
label={presentation.string.Save}
|
||||
kind="primary"
|
||||
size="large"
|
||||
on:click={tfaEnabled ? verifyAndDisable : verifyAndEnable}
|
||||
disabled={isLoading || code.length !== 6}
|
||||
/>
|
||||
<Status {status} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -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<Resources> => ({
|
||||
AddSocialId,
|
||||
AddEmailSocialId,
|
||||
EmployeeRefEditor,
|
||||
UserRoleSelect
|
||||
UserRoleSelect,
|
||||
TwoFactorSettings
|
||||
},
|
||||
actionImpl: {
|
||||
DeleteMixin
|
||||
|
||||
@@ -199,7 +199,8 @@ export default plugin(settingId, {
|
||||
Export: '' as Ref<Doc>,
|
||||
OfficeSettings: '' as Ref<Doc>,
|
||||
DisablePermissionsConfiguration: '' as Ref<Configuration>,
|
||||
Mailboxes: '' as Ref<Doc>
|
||||
Mailboxes: '' as Ref<Doc>,
|
||||
Security: '' as Ref<Doc>
|
||||
},
|
||||
mixin: {
|
||||
Editable: '' as Ref<Mixin<Editable>>,
|
||||
@@ -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,
|
||||
|
||||
@@ -127,6 +127,8 @@ export async function connect (title: string): Promise<Client | undefined> {
|
||||
break
|
||||
}
|
||||
|
||||
console.log('workspaceLoginInfo', workspaceLoginInfo)
|
||||
|
||||
const token = workspaceLoginInfo.token
|
||||
|
||||
setMetadata(presentation.metadata.Token, workspaceLoginInfo.token)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
`
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<string, string> = 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<void> {
|
||||
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<void> {
|
||||
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<LoginInfo> {
|
||||
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<Record<AccountMe
|
||||
changeUsername: wrap(changeUsername),
|
||||
updateWorkspaceName: wrap(updateWorkspaceName),
|
||||
deleteWorkspace: wrap(deleteWorkspace),
|
||||
generate2faSecret: wrap(generate2faSecret),
|
||||
enable2fa: wrap(enable2fa),
|
||||
disable2fa: wrap(disable2fa),
|
||||
verify2fa: wrap(verify2fa),
|
||||
updateWorkspaceRole: wrap(updateWorkspaceRole),
|
||||
updateAllowReadOnlyGuests: wrap(updateAllowReadOnlyGuests),
|
||||
updateAllowGuestSignUp: wrap(updateAllowGuestSignUp),
|
||||
|
||||
@@ -65,6 +65,7 @@ export interface Account {
|
||||
salt?: Buffer | null
|
||||
maxWorkspaces?: number
|
||||
failedLoginAttempts?: number // Number of consecutive failed login attempts
|
||||
tfaSecret?: string
|
||||
}
|
||||
|
||||
// TODO: type data with generic type
|
||||
@@ -432,6 +433,7 @@ export interface LoginInfo {
|
||||
name?: string
|
||||
socialId?: PersonId
|
||||
token?: string
|
||||
tfaRequired?: boolean
|
||||
}
|
||||
|
||||
export interface LoginInfoRequestData {
|
||||
|
||||
@@ -39,6 +39,7 @@ import platform, { getMetadata, PlatformError, Severity, Status, translate } fro
|
||||
import { getDBClient, setDBExtraOptions } from '@hcengineering/postgres'
|
||||
import { pbkdf2Sync, randomBytes } from 'crypto'
|
||||
import otpGenerator from 'otp-generator'
|
||||
import { authenticator } from 'otplib'
|
||||
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { decodeTokenVerbose, generateToken, type PermissionsGrant, TokenError } from '@hcengineering/server-token'
|
||||
@@ -2118,3 +2119,15 @@ export async function doMergeAccounts (
|
||||
await db.setPassword(primaryAccount, secondaryAccountObj.hash, secondaryAccountObj.salt)
|
||||
}
|
||||
}
|
||||
|
||||
export function generateTotpSecret (): string {
|
||||
return authenticator.generateSecret()
|
||||
}
|
||||
|
||||
export function verifyTotpCode (secret: string, code: string): boolean {
|
||||
return authenticator.check(code, secret)
|
||||
}
|
||||
|
||||
export function getTotpUrl (account: string, issuer: string, secret: string): string {
|
||||
return authenticator.keyuri(account, issuer, secret)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user