diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json
index 81f4722c6..df27633c6 100644
--- a/dashboard/package-lock.json
+++ b/dashboard/package-lock.json
@@ -136,422 +136,6 @@
"tslib": "^2.4.0"
}
},
- "node_modules/@esbuild/aix-ppc64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
- "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
- "cpu": [
- "ppc64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
- "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
- "cpu": [
- "arm"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
- "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-x64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
- "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-arm64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
- "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
- "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
- "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
- "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
- "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
- "cpu": [
- "arm"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
- "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
- "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
- "cpu": [
- "ia32"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
- "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
- "cpu": [
- "loong64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
- "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
- "cpu": [
- "mips64el"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
- "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
- "cpu": [
- "ppc64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
- "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
- "cpu": [
- "riscv64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
- "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
- "cpu": [
- "s390x"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-x64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
- "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-arm64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
- "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
- "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-arm64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
- "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
- "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openharmony-arm64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
- "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
- "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
- "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
- "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
- "cpu": [
- "ia32"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-x64": {
- "version": "0.27.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
- "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
@@ -3974,136 +3558,6 @@
"tslib": "^2.4.0"
}
},
- "@esbuild/aix-ppc64": {
- "version": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
- "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
- "optional": true
- },
- "@esbuild/android-arm": {
- "version": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
- "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
- "optional": true
- },
- "@esbuild/android-arm64": {
- "version": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
- "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
- "optional": true
- },
- "@esbuild/android-x64": {
- "version": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
- "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
- "optional": true
- },
- "@esbuild/darwin-arm64": {
- "version": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
- "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
- "optional": true
- },
- "@esbuild/darwin-x64": {
- "version": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
- "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
- "optional": true
- },
- "@esbuild/freebsd-arm64": {
- "version": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
- "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
- "optional": true
- },
- "@esbuild/freebsd-x64": {
- "version": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
- "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
- "optional": true
- },
- "@esbuild/linux-arm": {
- "version": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
- "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
- "optional": true
- },
- "@esbuild/linux-arm64": {
- "version": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
- "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
- "optional": true
- },
- "@esbuild/linux-ia32": {
- "version": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
- "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
- "optional": true
- },
- "@esbuild/linux-loong64": {
- "version": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
- "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
- "optional": true
- },
- "@esbuild/linux-mips64el": {
- "version": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
- "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
- "optional": true
- },
- "@esbuild/linux-ppc64": {
- "version": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
- "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
- "optional": true
- },
- "@esbuild/linux-riscv64": {
- "version": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
- "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
- "optional": true
- },
- "@esbuild/linux-s390x": {
- "version": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
- "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
- "optional": true
- },
- "@esbuild/linux-x64": {
- "version": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
- "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
- "optional": true
- },
- "@esbuild/netbsd-arm64": {
- "version": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
- "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
- "optional": true
- },
- "@esbuild/netbsd-x64": {
- "version": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
- "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
- "optional": true
- },
- "@esbuild/openbsd-arm64": {
- "version": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
- "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
- "optional": true
- },
- "@esbuild/openbsd-x64": {
- "version": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
- "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
- "optional": true
- },
- "@esbuild/openharmony-arm64": {
- "version": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
- "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
- "optional": true
- },
- "@esbuild/sunos-x64": {
- "version": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
- "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
- "optional": true
- },
- "@esbuild/win32-arm64": {
- "version": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
- "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
- "optional": true
- },
- "@esbuild/win32-ia32": {
- "version": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
- "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
- "optional": true
- },
- "@esbuild/win32-x64": {
- "version": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
- "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
- "optional": true
- },
"@eslint-community/eslint-utils": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
diff --git a/dashboard/src/Index.vue b/dashboard/src/Index.vue
index 4deabdc63..5bb88212e 100644
--- a/dashboard/src/Index.vue
+++ b/dashboard/src/Index.vue
@@ -459,8 +459,8 @@ onMounted(async () => {
ready.value = true;
- // when done, redirect the user to setup 2fa if it is mandatory and neither totp (twoFactorAuthenticationEnabled) nor passkey is setup
- if (config.value.mandatory2FA && !profile.value.twoFactorAuthenticationEnabled) {
+ // when done, redirect the user to setup 2fa if it is mandatory and neither totp (totpEnabled) nor passkey is setup
+ if (config.value.mandatory2FA && !profile.value.totpEnabled) {
const [error, result] = await profileModel.getPasskey();
if (error) return window.cloudron.onError(error);
diff --git a/dashboard/src/models/UsersModel.js b/dashboard/src/models/UsersModel.js
index 3068c70b7..4afc76340 100644
--- a/dashboard/src/models/UsersModel.js
+++ b/dashboard/src/models/UsersModel.js
@@ -199,7 +199,7 @@ function create() {
if (result.status !== 200) return [result];
return [null, result.body.inviteLink];
},
- async disableTwoFactorAuthentication(id) {
+ async disableTotp(id) {
let result;
try {
result = await fetcher.post(`${API_ORIGIN}/api/v1/users/${id}/totp_disable`, {}, { access_token: accessToken });
diff --git a/dashboard/src/views/ProfileView.vue b/dashboard/src/views/ProfileView.vue
index 05b905200..0d8eed623 100644
--- a/dashboard/src/views/ProfileView.vue
+++ b/dashboard/src/views/ProfileView.vue
@@ -102,7 +102,7 @@ async function onRevokeAllWebAndCliTokens() {
const userPasskey = ref(null);
const enableTwoFADialog = useTemplateRef('enableTwoFADialog');
-const has2FA = computed(() => profile.value.twoFactorAuthenticationEnabled || !!userPasskey.value);
+const has2FA = computed(() => profile.value.totpEnabled || !!userPasskey.value);
async function loadPasskey() {
const [error, result] = await profileModel.getPasskey();
@@ -164,7 +164,7 @@ onMounted(async () => {
await loadPasskey();
// check if we should show the 2fa setup
- if (config.value.mandatory2FA && !profile.value.twoFactorAuthenticationEnabled && !userPasskey.value) {
+ if (config.value.mandatory2FA && !profile.value.totpEnabled && !userPasskey.value) {
onOpenTwoFASetupDialog();
}
});
@@ -232,7 +232,7 @@ onMounted(async () => {
{{ $t('profile.twoFactorAuth.disabled') }}
- {{ $t('profile.twoFactorAuth.totpEnabled') }}
+ {{ $t('profile.twoFactorAuth.totpEnabled') }}
{{ $t('profile.twoFactorAuth.passkeyEnabled') }}
diff --git a/dashboard/src/views/UsersView.vue b/dashboard/src/views/UsersView.vue
index 3ca79ebe8..f771b6514 100644
--- a/dashboard/src/views/UsersView.vue
+++ b/dashboard/src/views/UsersView.vue
@@ -71,7 +71,7 @@ function createUserActionMenu(user) {
}, {
icon: 'fa-solid fa-qrcode',
label: t('users.passwordResetDialog.reset2FAAction'),
- visible: user.twoFactorAuthenticationEnabled && !(user.source && external2FA),
+ visible: user.totpEnabled && !(user.source && external2FA),
disabled: !canEdit(user),
action: on2FAReset.bind(null, user),
}, {
@@ -212,10 +212,10 @@ async function on2FAReset(user) {
if (!yes) return;
- const [error] = await usersModel.disableTwoFactorAuthentication(user.id);
+ const [error] = await usersModel.disableTotp(user.id);
if (error) return console.error(error);
- user.twoFactorAuthenticationEnabled = false;
+ user.totpEnabled = false;
}
function onEditOrAddUser(user = null) {
diff --git a/migrations/20260316000000-users-rename-twofactor-to-totp.js b/migrations/20260316000000-users-rename-twofactor-to-totp.js
new file mode 100644
index 000000000..6940ed0bd
--- /dev/null
+++ b/migrations/20260316000000-users-rename-twofactor-to-totp.js
@@ -0,0 +1,15 @@
+'use strict';
+
+exports.up = function(db, callback) {
+ db.runSql('ALTER TABLE users CHANGE COLUMN twoFactorAuthenticationEnabled totpEnabled BOOLEAN DEFAULT false, CHANGE COLUMN twoFactorAuthenticationSecret totpSecret VARCHAR(128) DEFAULT ""', function (error) {
+ if (error) console.error(error);
+ callback(error);
+ });
+};
+
+exports.down = function(db, callback) {
+ db.runSql('ALTER TABLE users CHANGE COLUMN totpEnabled twoFactorAuthenticationEnabled BOOLEAN DEFAULT false, CHANGE COLUMN totpSecret twoFactorAuthenticationSecret VARCHAR(128) DEFAULT ""', function (error) {
+ if (error) console.error(error);
+ callback(error);
+ });
+};
diff --git a/migrations/schema.sql b/migrations/schema.sql
index e180cdf42..0d06c8085 100644
--- a/migrations/schema.sql
+++ b/migrations/schema.sql
@@ -24,8 +24,8 @@ CREATE TABLE IF NOT EXISTS users(
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
displayName VARCHAR(512) DEFAULT "",
fallbackEmail VARCHAR(512) DEFAULT "",
- twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "",
- twoFactorAuthenticationEnabled BOOLEAN DEFAULT false,
+ totpSecret VARCHAR(128) DEFAULT "",
+ totpEnabled BOOLEAN DEFAULT false,
source VARCHAR(128) DEFAULT "",
role VARCHAR(32),
inviteToken VARCHAR(128) DEFAULT "", // one time token until user sets up account
diff --git a/src/directoryserver.js b/src/directoryserver.js
index dd48ea18b..a43318334 100644
--- a/src/directoryserver.js
+++ b/src/directoryserver.js
@@ -233,7 +233,7 @@ async function userSearch(req, res, next) {
}
};
- if (user.twoFactorAuthenticationEnabled) obj.attributes.twoFactorAuthenticationEnabled = true;
+ if (user.totpEnabled) obj.attributes.totpEnabled = true;
// ensure all filter values are also lowercase
const lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null);
diff --git a/src/externalldap.js b/src/externalldap.js
index 4ebeb157b..e12281c49 100644
--- a/src/externalldap.js
+++ b/src/externalldap.js
@@ -33,7 +33,7 @@ function translateUser(ldapConfig, ldapUser) {
const user = {
username: typeof ldapUser[ldapConfig.usernameField] === 'string' ? ldapUser[ldapConfig.usernameField].toLowerCase() : '',
email: ldapUser.mail || ldapUser.mailPrimaryAddress,
- twoFactorAuthenticationEnabled: !!ldapUser.twoFactorAuthenticationEnabled,
+ totpEnabled: !!ldapUser.totpEnabled,
displayName: ldapUser.displayName || ldapUser.cn // user.giveName + ' ' + user.sn
};
diff --git a/src/oidcserver.js b/src/oidcserver.js
index 0c8f64955..264b09aa3 100644
--- a/src/oidcserver.js
+++ b/src/oidcserver.js
@@ -412,7 +412,7 @@ async function interactionLogin(req, res, next) {
// If password verified but 2FA is required and not provided, return challenge
if (!verifyError && user && !user.ghost && !totpToken && !passkeyResponse) {
const userPasskeys = await passkeys.listByUserId(user.id);
- const has2FA = user.twoFactorAuthenticationEnabled || userPasskeys.length > 0;
+ const has2FA = user.totpEnabled || userPasskeys.length > 0;
if (has2FA) {
// Generate passkey options if user has passkeys
@@ -424,7 +424,7 @@ async function interactionLogin(req, res, next) {
return res.status(200).send({
twoFactorRequired: true,
- totpRequired: user.twoFactorAuthenticationEnabled,
+ totpRequired: user.totpEnabled,
passkeyOptions
});
}
diff --git a/src/passkeys.js b/src/passkeys.js
index a3ceeb418..bd14421a2 100644
--- a/src/passkeys.js
+++ b/src/passkeys.js
@@ -132,7 +132,7 @@ async function getRegistrationOptions(user) {
const existingPasskeys = await listByUserId(user.id);
if (existingPasskeys.length > 0) throw new BoxError(BoxError.ALREADY_EXISTS, 'User already has a passkey registered');
- if (user.twoFactorAuthenticationEnabled) throw new BoxError(BoxError.ALREADY_EXISTS, 'Cannot register passkey when TOTP is enabled');
+ if (user.totpEnabled) throw new BoxError(BoxError.ALREADY_EXISTS, 'Cannot register passkey when TOTP is enabled');
const { rpId, rpName } = await getRpInfo();
@@ -164,7 +164,7 @@ async function verifyRegistration(user, response, name) {
// check should ideally be in a transaction
const existingPasskeys = await listByUserId(user.id);
if (existingPasskeys.length > 0) throw new BoxError(BoxError.ALREADY_EXISTS, 'User already has a passkey registered');
- if (user.twoFactorAuthenticationEnabled) throw new BoxError(BoxError.ALREADY_EXISTS, 'Cannot register passkey when TOTP is enabled');
+ if (user.totpEnabled) throw new BoxError(BoxError.ALREADY_EXISTS, 'Cannot register passkey when TOTP is enabled');
const expectedChallenge = getAndDeleteChallenge(user.id);
if (!expectedChallenge) throw new BoxError(BoxError.BAD_FIELD, 'Challenge expired or not found');
diff --git a/src/routes/auth.js b/src/routes/auth.js
index f499e8f5b..8216889ea 100644
--- a/src/routes/auth.js
+++ b/src/routes/auth.js
@@ -65,10 +65,10 @@ async function passwordReset(req, res, next) {
if (getError) return next(new HttpError(401, 'Invalid resetToken'));
if (!userObject) return next(new HttpError(401, 'Invalid resetToken'));
- if (userObject.twoFactorAuthenticationEnabled) {
+ if (userObject.totpEnabled) {
if (typeof req.body.totpToken !== 'string') return next(new HttpError(401, 'A totpToken must be provided'));
- const verified = speakeasy.totp.verify({ secret: userObject.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken, window: 2 });
+ const verified = speakeasy.totp.verify({ secret: userObject.totpSecret, encoding: 'base32', token: req.body.totpToken, window: 2 });
if (!verified) return next(new HttpError(401, 'Invalid totpToken'));
}
diff --git a/src/routes/profile.js b/src/routes/profile.js
index e765c688d..f50199988 100644
--- a/src/routes/profile.js
+++ b/src/routes/profile.js
@@ -32,7 +32,7 @@ async function get(req, res, next) {
email: req.user.email,
fallbackEmail: req.user.fallbackEmail,
displayName: req.user.displayName,
- twoFactorAuthenticationEnabled: req.user.twoFactorAuthenticationEnabled,
+ totpEnabled: req.user.totpEnabled,
role: req.user.role,
source: req.user.source,
hasBackgroundImage: req.user.hasBackgroundImage,
@@ -178,31 +178,31 @@ async function setPassword(req, res, next) {
next(new HttpSuccess(204));
}
-async function setTwoFactorAuthenticationSecret(req, res, next) {
+async function setTotpSecret(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
- const [error, result] = await safe(users.setTwoFactorAuthenticationSecret(req.user, AuditSource.fromRequest(req)));
+ const [error, result] = await safe(users.setTotpSecret(req.user, AuditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { secret: result.secret, qrcode: result.qrcode }));
}
-async function enableTwoFactorAuthentication(req, res, next) {
+async function enableTotp(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.user, 'object');
if (!req.body.totpToken || typeof req.body.totpToken !== 'string') return next(new HttpError(400, 'totpToken must be a nonempty string'));
- const [error] = await safe(users.enableTwoFactorAuthentication(req.user, req.body.totpToken, AuditSource.fromRequest(req)));
+ const [error] = await safe(users.enableTotp(req.user, req.body.totpToken, AuditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204, {}));
}
-async function disableTwoFactorAuthentication(req, res, next) {
+async function disableTotp(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
- const [error] = await safe(users.disableTwoFactorAuthentication(req.user, AuditSource.fromRequest(req)));
+ const [error] = await safe(users.disableTotp(req.user, AuditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204, {}));
@@ -296,9 +296,9 @@ export default {
setBackgroundImage,
unsetBackgroundImage,
setPassword,
- setTwoFactorAuthenticationSecret,
- enableTwoFactorAuthentication,
- disableTwoFactorAuthentication,
+ setTotpSecret,
+ enableTotp,
+ disableTotp,
setNotificationConfig,
destroyUserSession,
getPasskey,
diff --git a/src/routes/users.js b/src/routes/users.js
index 36943f80f..afce72740 100644
--- a/src/routes/users.js
+++ b/src/routes/users.js
@@ -185,12 +185,12 @@ async function verifyPassword(req, res, next) {
next();
}
-async function disableTwoFactorAuthentication(req, res, next) {
+async function disableTotp(req, res, next) {
assert.strictEqual(typeof req.resources.user, 'object');
if (users.compareRoles(req.user.role, req.resources.user.role) < 0) return next(new HttpError(403, `role '${req.resources.user.role}' is required but user has only '${req.user.role}'`));
- const [error] = await safe(users.disableTwoFactorAuthentication(req.resources.user, AuditSource.fromRequest(req)));
+ const [error] = await safe(users.disableTotp(req.resources.user, AuditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
@@ -308,7 +308,7 @@ export default {
getInviteLink,
sendInviteEmail,
- disableTwoFactorAuthentication,
+ disableTotp,
load
};
diff --git a/src/server.js b/src/server.js
index 5eb96606c..c831b2bee 100644
--- a/src/server.js
+++ b/src/server.js
@@ -195,9 +195,9 @@ async function initializeExpressSync() {
router.post('/api/v1/profile/background_image', token, authorizeUser, multipart, routes.profile.setBackgroundImage); // backgroundImage is not exposed in LDAP. so it's personal and not locked
router.del ('/api/v1/profile/background_image', token, authorizeUser, routes.profile.unsetBackgroundImage); // backgroundImage is not exposed in LDAP. so it's personal and not locked
router.post('/api/v1/profile/password', json, token, authorizeUser, routes.users.verifyPassword, routes.profile.setPassword);
- router.post('/api/v1/profile/totp_secret', json, token, authorizeUser, routes.profile.setTwoFactorAuthenticationSecret);
- router.post('/api/v1/profile/totp_enable', json, token, authorizeUser, routes.profile.enableTwoFactorAuthentication);
- router.post('/api/v1/profile/totp_disable', json, token, authorizeUser, routes.users.verifyPassword, routes.profile.disableTwoFactorAuthentication);
+ router.post('/api/v1/profile/totp_secret', json, token, authorizeUser, routes.profile.setTotpSecret);
+ router.post('/api/v1/profile/totp_enable', json, token, authorizeUser, routes.profile.enableTotp);
+ router.post('/api/v1/profile/totp_disable', json, token, authorizeUser, routes.users.verifyPassword, routes.profile.disableTotp);
router.post('/api/v1/profile/notification_config', json, token, authorizeUser, routes.profile.setNotificationConfig); // non-admins cannot get notifications anyway
router.del ('/api/v1/profile/sessions', token, authorizeUser, routes.profile.destroyUserSession);
@@ -237,7 +237,7 @@ async function initializeExpressSync() {
router.post('/api/v1/users/:userId/send_password_reset_email', json, token, authorizeUserManager, routes.users.load, routes.users.sendPasswordResetEmail);
router.get ('/api/v1/users/:userId/invite_link', token, authorizeUserManager, routes.users.load, routes.users.getInviteLink);
router.post('/api/v1/users/:userId/send_invite_email', json, token, authorizeUserManager, routes.users.load, routes.users.sendInviteEmail);
- router.post('/api/v1/users/:userId/totp_disable', json, token, authorizeUserManager, routes.users.load, routes.users.disableTwoFactorAuthentication);
+ router.post('/api/v1/users/:userId/totp_disable', json, token, authorizeUserManager, routes.users.load, routes.users.disableTotp);
// Group management
router.get ('/api/v1/groups', token, authorizeUserManager, routes.groups.list);
diff --git a/src/test/directoryserver-test.js b/src/test/directoryserver-test.js
index 46b0700d7..662149aaf 100644
--- a/src/test/directoryserver-test.js
+++ b/src/test/directoryserver-test.js
@@ -135,9 +135,9 @@ describe('Directory Server (LDAP)', function () {
// the 2fa is requested by client by passing `+totpToken=xxx` . totpToken
// is ignored if the user has no 2fa setup. If present, it is validated.
it('enable 2fa for admin', async function () {
- twofa = await users.setTwoFactorAuthenticationSecret(admin, auditSource);
+ twofa = await users.setTotpSecret(admin, auditSource);
const totpToken = speakeasy.totp({ secret: twofa.secret, encoding: 'base32' });
- await users.enableTwoFactorAuthentication(await users.get(admin.id), totpToken, auditSource);
+ await users.enableTotp(await users.get(admin.id), totpToken, auditSource);
});
it('totp check not requested - fails with bad password', async function () {
diff --git a/src/test/passkeys-test.js b/src/test/passkeys-test.js
index 974b4832a..863084563 100644
--- a/src/test/passkeys-test.js
+++ b/src/test/passkeys-test.js
@@ -180,19 +180,19 @@ describe('Passkeys', function () {
const adminUser = await users.get(admin.id);
// enable TOTP first
- const twofa = await users.setTwoFactorAuthenticationSecret(adminUser, auditSource);
- adminUser.twoFactorAuthenticationSecret = twofa.secret;
+ const twofa = await users.setTotpSecret(adminUser, auditSource);
+ adminUser.totpSecret = twofa.secret;
const totpToken = speakeasy.totp({ secret: twofa.secret, encoding: 'base32' });
- await users.enableTwoFactorAuthentication(adminUser, totpToken, auditSource);
- adminUser.twoFactorAuthenticationEnabled = true;
+ await users.enableTotp(adminUser, totpToken, auditSource);
+ adminUser.totpEnabled = true;
const [error] = await safe(passkeys.getRegistrationOptions(adminUser));
assert.notEqual(error, null);
assert.equal(error.reason, BoxError.ALREADY_EXISTS);
// disable TOTP for further tests
- await users.disableTwoFactorAuthentication(adminUser, auditSource);
- adminUser.twoFactorAuthenticationEnabled = false;
+ await users.disableTotp(adminUser, auditSource);
+ adminUser.totpEnabled = false;
});
});
@@ -293,11 +293,11 @@ describe('Passkeys', function () {
await passkeys.verifyRegistration(adminUser, response, 'Exclusion Test');
// try to enable TOTP
- const twofa = await users.setTwoFactorAuthenticationSecret(adminUser, auditSource);
- adminUser.twoFactorAuthenticationSecret = twofa.secret;
+ const twofa = await users.setTotpSecret(adminUser, auditSource);
+ adminUser.totpSecret = twofa.secret;
const totpToken = speakeasy.totp({ secret: twofa.secret, encoding: 'base32' });
- const [error] = await safe(users.enableTwoFactorAuthentication(adminUser, totpToken, auditSource));
+ const [error] = await safe(users.enableTotp(adminUser, totpToken, auditSource));
assert.notEqual(error, null);
assert.equal(error.reason, BoxError.ALREADY_EXISTS);
@@ -308,19 +308,19 @@ describe('Passkeys', function () {
const adminUser = await users.get(admin.id);
// enable TOTP
- const twofa = await users.setTwoFactorAuthenticationSecret(adminUser, auditSource);
- adminUser.twoFactorAuthenticationSecret = twofa.secret;
+ const twofa = await users.setTotpSecret(adminUser, auditSource);
+ adminUser.totpSecret = twofa.secret;
const totpToken = speakeasy.totp({ secret: twofa.secret, encoding: 'base32' });
- await users.enableTwoFactorAuthentication(adminUser, totpToken, auditSource);
- adminUser.twoFactorAuthenticationEnabled = true;
+ await users.enableTotp(adminUser, totpToken, auditSource);
+ adminUser.totpEnabled = true;
const [error] = await safe(passkeys.getRegistrationOptions(adminUser));
assert.notEqual(error, null);
assert.equal(error.reason, BoxError.ALREADY_EXISTS);
// cleanup
- await users.disableTwoFactorAuthentication(adminUser, auditSource);
- adminUser.twoFactorAuthenticationEnabled = false;
+ await users.disableTotp(adminUser, auditSource);
+ adminUser.totpEnabled = false;
});
});
});
diff --git a/src/test/users-test.js b/src/test/users-test.js
index cde1ed0dc..603824dde 100644
--- a/src/test/users-test.js
+++ b/src/test/users-test.js
@@ -384,35 +384,35 @@ describe('User', function () {
let twofa;
it('create secret', async function () {
- twofa = await users.setTwoFactorAuthenticationSecret(admin, auditSource);
+ twofa = await users.setTotpSecret(admin, auditSource);
assert.equal(typeof twofa.secret, 'string');
assert.equal(typeof twofa.qrcode, 'string');
});
it('can create secret again', async function () {
- twofa = await users.setTwoFactorAuthenticationSecret(admin, auditSource);
+ twofa = await users.setTotpSecret(admin, auditSource);
assert.equal(typeof twofa.secret, 'string');
assert.equal(typeof twofa.qrcode, 'string');
- admin.twoFactorAuthenticationSecret = twofa.secret; // update user object
+ admin.totpSecret = twofa.secret; // update user object
});
it('enable 2fa', async function () {
const totpToken = speakeasy.totp({ secret: twofa.secret, encoding: 'base32' });
- await users.enableTwoFactorAuthentication(admin, totpToken, auditSource);
+ await users.enableTotp(admin, totpToken, auditSource);
const u = await users.get(admin.id);
- assert.equal(u.twoFactorAuthenticationEnabled, true);
- admin.twoFactorAuthenticationEnabled = true; // update user object
+ assert.equal(u.totpEnabled, true);
+ admin.totpEnabled = true; // update user object
});
it('cannot re-create secret', async function () {
- const [error] = await safe(users.setTwoFactorAuthenticationSecret(admin, auditSource));
+ const [error] = await safe(users.setTotpSecret(admin, auditSource));
assert.equal(error.reason, BoxError.ALREADY_EXISTS);
});
it('cannot re-enable 2fa', async function () {
const totpToken = speakeasy.totp({ secret: twofa.secret, encoding: 'base32' });
- const [error] = await safe(users.enableTwoFactorAuthentication(admin, totpToken, auditSource));
+ const [error] = await safe(users.enableTotp(admin, totpToken, auditSource));
assert.equal(error.reason, BoxError.ALREADY_EXISTS);
});
diff --git a/src/user-directory.js b/src/user-directory.js
index ceb81264f..022e2832a 100644
--- a/src/user-directory.js
+++ b/src/user-directory.js
@@ -35,7 +35,7 @@ async function setProfileConfig(profileConfig, options, auditSource) {
const allUsers = await users.list();
for (const user of allUsers) {
- if (user.twoFactorAuthenticationEnabled) continue;
+ if (user.totpEnabled) continue;
if (options.persistUserIdSessions === user.id) continue; // do not logout the API caller
if (!user.username) continue; // if a user has no username set yet
diff --git a/src/users.js b/src/users.js
index fb254a7e6..b03ea26b5 100644
--- a/src/users.js
+++ b/src/users.js
@@ -43,7 +43,7 @@ const ROLE_OWNER = 'owner';
// the avatar and backgroundImage fields are special and not added here to reduce response sizes
const USERS_FIELDS = [ 'id', 'username', 'email', 'fallbackEmail', 'password', 'salt', 'creationTime', 'inviteToken', 'resetToken', 'displayName', 'language',
- 'twoFactorAuthenticationEnabled', 'twoFactorAuthenticationSecret', 'active', 'source', 'role', 'resetTokenCreationTime', 'loginLocationsJson', 'notificationConfigJson',
+ 'totpEnabled', 'totpSecret', 'active', 'source', 'role', 'resetTokenCreationTime', 'loginLocationsJson', 'notificationConfigJson',
'(avatar IS NOT NULL) AS hasAvatar', '(backgroundImage IS NOT NULL) AS hasBackgroundImage' ].join(',');
const DEFAULT_GHOST_LIFETIME = 6 * 60 * 60 * 1000; // 6 hours
@@ -59,7 +59,7 @@ const randomBytesAsync = util.promisify(crypto.randomBytes);
function postProcess(result) {
assert.strictEqual(typeof result, 'object');
- result.twoFactorAuthenticationEnabled = !!result.twoFactorAuthenticationEnabled;
+ result.totpEnabled = !!result.totpEnabled;
result.active = !!result.active;
result.loginLocations = safe.JSON.parse(result.loginLocationsJson) || [];
@@ -136,7 +136,7 @@ function validatePassword(password) {
function removePrivateFields(user) {
const result = _.pick(user, [
'id', 'username', 'email', 'fallbackEmail', 'displayName', 'groupIds', 'active', 'source', 'role', 'createdAt',
- 'twoFactorAuthenticationEnabled', 'notificationConfig', 'hasAvatar', 'hasBackgroundImage' ]);
+ 'totpEnabled', 'notificationConfig', 'hasAvatar', 'hasBackgroundImage' ]);
result.inviteAccepted = !user.inviteToken; // invite status indicator
return result;
@@ -395,7 +395,7 @@ async function update(user, data, auditSource) {
assert.strictEqual(typeof data, 'object');
assert(auditSource && typeof auditSource === 'object');
- assert(!('twoFactorAuthenticationEnabled' in data) || (typeof data.twoFactorAuthenticationEnabled === 'boolean'));
+ assert(!('totpEnabled' in data) || (typeof data.totpEnabled === 'boolean'));
assert(!('active' in data) || (typeof data.active === 'boolean'));
assert(!('loginLocations' in data) || (Array.isArray(data.loginLocations)));
@@ -434,7 +434,7 @@ async function update(user, data, auditSource) {
const args = [], fields = [];
for (const k in data) {
- if (k === 'twoFactorAuthenticationEnabled' || k === 'active') {
+ if (k === 'totpEnabled' || k === 'active') {
fields.push(k + ' = ?');
args.push(data[k] ? 1 : 0);
} else if (k === 'loginLocations' || k === 'notificationConfig') {
@@ -626,7 +626,7 @@ async function verify(user, password, identifier, options) {
if (user.source === 'ldap') {
await externalLdap.verifyPassword(user.username, password, options);
const externalLdapConfig = await externalLdap.getConfig();
- localTotpCheck = user.twoFactorAuthenticationEnabled && !externalLdap.supports2FA(externalLdapConfig);
+ localTotpCheck = user.totpEnabled && !externalLdap.supports2FA(externalLdapConfig);
} else {
const saltBinary = Buffer.from(user.salt, 'hex');
const [cryptoError, derivedKey] = await safe(pbkdf2Async(password, saltBinary, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, CRYPTO_DIGEST));
@@ -638,12 +638,12 @@ async function verify(user, password, identifier, options) {
throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Wrong password');
}
- localTotpCheck = user.twoFactorAuthenticationEnabled;
+ localTotpCheck = user.totpEnabled;
}
if (localTotpCheck && !options.skipTotpCheck) {
if (!options.totpToken) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'A totpToken must be provided');
- const verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: options.totpToken, window: 2 });
+ const verified = speakeasy.totp.verify({ secret: user.totpSecret, encoding: 'base32', token: options.totpToken, window: 2 });
if (!verified) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Invalid totpToken');
}
@@ -911,7 +911,7 @@ async function setupAccount(user, data, auditSource) {
return result.accessToken;
}
-async function setTwoFactorAuthenticationSecret(user, auditSource) {
+async function setTotpSecret(user, auditSource) {
assert.strictEqual(typeof user, 'object');
assert(auditSource && typeof auditSource === 'object');
@@ -920,12 +920,12 @@ async function setTwoFactorAuthenticationSecret(user, auditSource) {
if (constants.DEMO && user.username === constants.DEMO_USERNAME) throw new BoxError(BoxError.BAD_STATE, 'Not allowed in demo mode');
- if (user.twoFactorAuthenticationEnabled) throw new BoxError(BoxError.ALREADY_EXISTS, '2FA is already enabled');
+ if (user.totpEnabled) throw new BoxError(BoxError.ALREADY_EXISTS, '2FA is already enabled');
const cloudronName = await branding.getCloudronName();
const secret = speakeasy.generateSecret({ name: `${cloudronName}` });
- await update(user, { twoFactorAuthenticationSecret: secret.base32, twoFactorAuthenticationEnabled: false }, auditSource);
+ await update(user, { totpSecret: secret.base32, totpEnabled: false }, auditSource);
const [error, dataUrl] = await safe(qrcode.toDataURL(secret.otpauth_url));
if (error) throw new BoxError(BoxError.INTERNAL_ERROR, error);
@@ -933,7 +933,7 @@ async function setTwoFactorAuthenticationSecret(user, auditSource) {
return { secret: secret.base32, qrcode: dataUrl };
}
-async function enableTwoFactorAuthentication(user, totpToken, auditSource) {
+async function enableTotp(user, totpToken, auditSource) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof totpToken, 'string');
assert(auditSource && typeof auditSource === 'object');
@@ -945,22 +945,22 @@ async function enableTwoFactorAuthentication(user, totpToken, auditSource) {
const userPasskeys = await passkeys.listByUserId(user.id);
if (userPasskeys.length > 0) throw new BoxError(BoxError.ALREADY_EXISTS, 'Cannot enable TOTP when passkey is registered');
- const verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: totpToken, window: 2 });
+ const verified = speakeasy.totp.verify({ secret: user.totpSecret, encoding: 'base32', token: totpToken, window: 2 });
if (!verified) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Invalid 2FA code');
- if (user.twoFactorAuthenticationEnabled) throw new BoxError(BoxError.ALREADY_EXISTS, '2FA already enabled');
+ if (user.totpEnabled) throw new BoxError(BoxError.ALREADY_EXISTS, '2FA already enabled');
- await update(user, { twoFactorAuthenticationEnabled: true }, auditSource);
+ await update(user, { totpEnabled: true }, auditSource);
}
-async function disableTwoFactorAuthentication(user, auditSource) {
+async function disableTotp(user, auditSource) {
assert.strictEqual(typeof user, 'object');
assert(auditSource && typeof auditSource === 'object');
const externalLdapConfig = await externalLdap.getConfig();
if (user.source === 'ldap' && externalLdap.supports2FA(externalLdapConfig)) throw new BoxError(BoxError.BAD_STATE, 'Cannot disable 2FA of external auth user');
- await update(user, { twoFactorAuthenticationEnabled: false, twoFactorAuthenticationSecret: '' }, auditSource);
+ await update(user, { totpEnabled: false, totpSecret: '' }, auditSource);
}
async function getAvatar(user) {
@@ -1042,9 +1042,9 @@ export default {
updateProfile,
update,
del,
- setTwoFactorAuthenticationSecret,
- enableTwoFactorAuthentication,
- disableTwoFactorAuthentication,
+ setTotpSecret,
+ enableTotp,
+ disableTotp,
sendPasswordResetByIdentifier,
getPasswordResetLink,
sendPasswordResetEmail,