Usage of TOTP Code or Recovery code on login. If recovery code then mark that code as used. Setup disabling of 2FA if a current password is entered.

This commit is contained in:
Bradley Shellnut 2024-04-11 17:17:45 -07:00
parent 389fddc32e
commit 53f3b99133
11 changed files with 285 additions and 152 deletions

View file

@ -30,8 +30,8 @@
"@sveltejs/kit": "^2.5.5", "@sveltejs/kit": "^2.5.5",
"@sveltejs/vite-plugin-svelte": "^3.0.2", "@sveltejs/vite-plugin-svelte": "^3.0.2",
"@types/cookie": "^0.6.0", "@types/cookie": "^0.6.0",
"@types/node": "^20.12.5", "@types/node": "^20.12.6",
"@types/pg": "^8.11.4", "@types/pg": "^8.11.5",
"@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0", "@typescript-eslint/parser": "^6.21.0",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
@ -39,7 +39,7 @@
"drizzle-kit": "^0.20.14", "drizzle-kit": "^0.20.14",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1", "eslint-plugin-svelte": "^2.36.0",
"just-clone": "^6.2.0", "just-clone": "^6.2.0",
"just-debounce-it": "^3.2.0", "just-debounce-it": "^3.2.0",
"postcss": "^8.4.38", "postcss": "^8.4.38",
@ -54,12 +54,12 @@
"svelte": "^4.2.12", "svelte": "^4.2.12",
"svelte-check": "^3.6.9", "svelte-check": "^3.6.9",
"svelte-headless-table": "^0.18.2", "svelte-headless-table": "^0.18.2",
"svelte-meta-tags": "^3.1.1", "svelte-meta-tags": "^3.1.2",
"svelte-preprocess": "^5.1.3", "svelte-preprocess": "^5.1.3",
"svelte-sequential-preprocessor": "^2.0.1", "svelte-sequential-preprocessor": "^2.0.1",
"sveltekit-flash-message": "^2.4.4", "sveltekit-flash-message": "^2.4.4",
"sveltekit-rate-limiter": "^0.4.3", "sveltekit-rate-limiter": "^0.4.3",
"sveltekit-superforms": "^2.12.2", "sveltekit-superforms": "^2.12.4",
"tailwindcss": "^3.4.3", "tailwindcss": "^3.4.3",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tslib": "^2.6.1", "tslib": "^2.6.1",

View file

@ -58,13 +58,13 @@ dependencies:
version: 0.6.0 version: 0.6.0
drizzle-orm: drizzle-orm:
specifier: ^0.30.7 specifier: ^0.30.7
version: 0.30.7(@neondatabase/serverless@0.9.0)(@planetscale/database@1.16.0)(@types/pg@8.11.4)(pg@8.11.5)(postgres@3.4.4) version: 0.30.7(@neondatabase/serverless@0.9.0)(@planetscale/database@1.16.0)(@types/pg@8.11.5)(pg@8.11.5)(postgres@3.4.4)
feather-icons: feather-icons:
specifier: ^4.29.1 specifier: ^4.29.1
version: 4.29.1 version: 4.29.1
formsnap: formsnap:
specifier: ^0.5.1 specifier: ^0.5.1
version: 0.5.1(svelte@4.2.12)(sveltekit-superforms@2.12.2) version: 0.5.1(svelte@4.2.12)(sveltekit-superforms@2.12.4)
html-entities: html-entities:
specifier: ^2.5.2 specifier: ^2.5.2
version: 2.5.2 version: 2.5.2
@ -152,11 +152,11 @@ devDependencies:
specifier: ^0.6.0 specifier: ^0.6.0
version: 0.6.0 version: 0.6.0
'@types/node': '@types/node':
specifier: ^20.12.5 specifier: ^20.12.6
version: 20.12.5 version: 20.12.6
'@types/pg': '@types/pg':
specifier: ^8.11.4 specifier: ^8.11.5
version: 8.11.4 version: 8.11.5
'@typescript-eslint/eslint-plugin': '@typescript-eslint/eslint-plugin':
specifier: ^6.21.0 specifier: ^6.21.0
version: 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.4) version: 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.4)
@ -179,8 +179,8 @@ devDependencies:
specifier: ^9.1.0 specifier: ^9.1.0
version: 9.1.0(eslint@8.57.0) version: 9.1.0(eslint@8.57.0)
eslint-plugin-svelte: eslint-plugin-svelte:
specifier: ^2.35.1 specifier: ^2.36.0
version: 2.35.1(eslint@8.57.0)(svelte@4.2.12)(ts-node@10.9.2) version: 2.36.0(eslint@8.57.0)(svelte@4.2.12)(ts-node@10.9.2)
just-clone: just-clone:
specifier: ^6.2.0 specifier: ^6.2.0
version: 6.2.0 version: 6.2.0
@ -224,8 +224,8 @@ devDependencies:
specifier: ^0.18.2 specifier: ^0.18.2
version: 0.18.2(svelte@4.2.12) version: 0.18.2(svelte@4.2.12)
svelte-meta-tags: svelte-meta-tags:
specifier: ^3.1.1 specifier: ^3.1.2
version: 3.1.1(svelte@4.2.12)(typescript@5.4.4) version: 3.1.2(svelte@4.2.12)(typescript@5.4.4)
svelte-preprocess: svelte-preprocess:
specifier: ^5.1.3 specifier: ^5.1.3
version: 5.1.3(postcss-load-config@5.0.3)(postcss@8.4.38)(sass@1.74.1)(svelte@4.2.12)(typescript@5.4.4) version: 5.1.3(postcss-load-config@5.0.3)(postcss@8.4.38)(sass@1.74.1)(svelte@4.2.12)(typescript@5.4.4)
@ -239,14 +239,14 @@ devDependencies:
specifier: ^0.4.3 specifier: ^0.4.3
version: 0.4.3(@sveltejs/kit@2.5.5) version: 0.4.3(@sveltejs/kit@2.5.5)
sveltekit-superforms: sveltekit-superforms:
specifier: ^2.12.2 specifier: ^2.12.4
version: 2.12.2(@sveltejs/kit@2.5.5)(@types/json-schema@7.0.15)(esbuild-runner@2.2.2)(esbuild@0.20.2)(svelte@4.2.12) version: 2.12.4(@sveltejs/kit@2.5.5)(@types/json-schema@7.0.15)(esbuild-runner@2.2.2)(esbuild@0.20.2)(svelte@4.2.12)
tailwindcss: tailwindcss:
specifier: ^3.4.3 specifier: ^3.4.3
version: 3.4.3(ts-node@10.9.2) version: 3.4.3(ts-node@10.9.2)
ts-node: ts-node:
specifier: ^10.9.2 specifier: ^10.9.2
version: 10.9.2(@types/node@20.12.5)(typescript@5.4.4) version: 10.9.2(@types/node@20.12.6)(typescript@5.4.4)
tslib: tslib:
specifier: ^2.6.1 specifier: ^2.6.1
version: 2.6.2 version: 2.6.2
@ -258,10 +258,10 @@ devDependencies:
version: 5.4.4 version: 5.4.4
vite: vite:
specifier: ^5.2.8 specifier: ^5.2.8
version: 5.2.8(@types/node@20.12.5)(sass@1.74.1) version: 5.2.8(@types/node@20.12.6)(sass@1.74.1)
vitest: vitest:
specifier: ^1.4.0 specifier: ^1.4.0
version: 1.4.0(@types/node@20.12.5)(sass@1.74.1) version: 1.4.0(@types/node@20.12.6)(sass@1.74.1)
zod: zod:
specifier: ^3.22.4 specifier: ^3.22.4
version: 3.22.4 version: 3.22.4
@ -3286,7 +3286,7 @@ packages:
sirv: 2.0.4 sirv: 2.0.4
svelte: 4.2.12 svelte: 4.2.12
tiny-glob: 0.2.9 tiny-glob: 0.2.9
vite: 5.2.8(@types/node@20.12.5)(sass@1.74.1) vite: 5.2.8(@types/node@20.12.6)(sass@1.74.1)
/@sveltejs/vite-plugin-svelte-inspector@2.0.0(@sveltejs/vite-plugin-svelte@3.0.2)(svelte@4.2.12)(vite@5.2.8): /@sveltejs/vite-plugin-svelte-inspector@2.0.0(@sveltejs/vite-plugin-svelte@3.0.2)(svelte@4.2.12)(vite@5.2.8):
resolution: {integrity: sha512-gjr9ZFg1BSlIpfZ4PRewigrvYmHWbDrq2uvvPB1AmTWKuM+dI1JXQSUu2pIrYLb/QncyiIGkFDFKTwJ0XqQZZg==} resolution: {integrity: sha512-gjr9ZFg1BSlIpfZ4PRewigrvYmHWbDrq2uvvPB1AmTWKuM+dI1JXQSUu2pIrYLb/QncyiIGkFDFKTwJ0XqQZZg==}
@ -3299,7 +3299,7 @@ packages:
'@sveltejs/vite-plugin-svelte': 3.0.2(svelte@4.2.12)(vite@5.2.8) '@sveltejs/vite-plugin-svelte': 3.0.2(svelte@4.2.12)(vite@5.2.8)
debug: 4.3.4 debug: 4.3.4
svelte: 4.2.12 svelte: 4.2.12
vite: 5.2.8(@types/node@20.12.5)(sass@1.74.1) vite: 5.2.8(@types/node@20.12.6)(sass@1.74.1)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -3317,7 +3317,7 @@ packages:
magic-string: 0.30.5 magic-string: 0.30.5
svelte: 4.2.12 svelte: 4.2.12
svelte-hmr: 0.15.3(svelte@4.2.12) svelte-hmr: 0.15.3(svelte@4.2.12)
vite: 5.2.8(@types/node@20.12.5)(sass@1.74.1) vite: 5.2.8(@types/node@20.12.6)(sass@1.74.1)
vitefu: 0.2.5(vite@5.2.8) vitefu: 0.2.5(vite@5.2.8)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -3360,22 +3360,22 @@ packages:
/@types/json-schema@7.0.15: /@types/json-schema@7.0.15:
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
/@types/node@20.12.5: /@types/node@20.12.6:
resolution: {integrity: sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw==} resolution: {integrity: sha512-3KurE8taB8GCvZBPngVbp0lk5CKi8M9f9k1rsADh0Evdz5SzJ+Q+Hx9uHoFGsLnLnd1xmkDQr2hVhlA0Mn0lKQ==}
dependencies: dependencies:
undici-types: 5.26.5 undici-types: 5.26.5
/@types/pg@8.11.4: /@types/pg@8.11.5:
resolution: {integrity: sha512-yw3Bwbda6vO+NvI1Ue/YKOwtl31AYvvd/e73O3V4ZkNzuGpTDndLSyc0dQRB2xrQqDePd20pEGIfqSp/GH3pRw==} resolution: {integrity: sha512-2xMjVviMxneZHDHX5p5S6tsRRs7TpDHeeK7kTTMe/kAC/mRRNjWHjZg0rkiY+e17jXSZV3zJYDxXV8Cy72/Vuw==}
dependencies: dependencies:
'@types/node': 20.12.5 '@types/node': 20.12.6
pg-protocol: 1.6.0 pg-protocol: 1.6.0
pg-types: 4.0.2 pg-types: 4.0.2
/@types/pg@8.6.6: /@types/pg@8.6.6:
resolution: {integrity: sha512-O2xNmXebtwVekJDD+02udOncjVcMZQuTEQEMpKJ0ZRf5E7/9JJX3izhKUcUifBkyKpljyUM6BTgy2trmviKlpw==} resolution: {integrity: sha512-O2xNmXebtwVekJDD+02udOncjVcMZQuTEQEMpKJ0ZRf5E7/9JJX3izhKUcUifBkyKpljyUM6BTgy2trmviKlpw==}
dependencies: dependencies:
'@types/node': 20.12.5 '@types/node': 20.12.6
pg-protocol: 1.6.0 pg-protocol: 1.6.0
pg-types: 2.2.0 pg-types: 2.2.0
dev: false dev: false
@ -4312,7 +4312,7 @@ packages:
- supports-color - supports-color
dev: true dev: true
/drizzle-orm@0.30.7(@neondatabase/serverless@0.9.0)(@planetscale/database@1.16.0)(@types/pg@8.11.4)(pg@8.11.5)(postgres@3.4.4): /drizzle-orm@0.30.7(@neondatabase/serverless@0.9.0)(@planetscale/database@1.16.0)(@types/pg@8.11.5)(pg@8.11.5)(postgres@3.4.4):
resolution: {integrity: sha512-9qefSZQlu2fO2qv24piHyWFWcxcOY15//0v4j8qomMqaxzipNoG+fUBrQ7Ftk7PY7APRbRdn/nkEXWxiI4a8mw==} resolution: {integrity: sha512-9qefSZQlu2fO2qv24piHyWFWcxcOY15//0v4j8qomMqaxzipNoG+fUBrQ7Ftk7PY7APRbRdn/nkEXWxiI4a8mw==}
peerDependencies: peerDependencies:
'@aws-sdk/client-rds-data': '>=3' '@aws-sdk/client-rds-data': '>=3'
@ -4394,7 +4394,7 @@ packages:
dependencies: dependencies:
'@neondatabase/serverless': 0.9.0 '@neondatabase/serverless': 0.9.0
'@planetscale/database': 1.16.0 '@planetscale/database': 1.16.0
'@types/pg': 8.11.4 '@types/pg': 8.11.5
pg: 8.11.5 pg: 8.11.5
postgres: 3.4.4 postgres: 3.4.4
dev: false dev: false
@ -4613,13 +4613,14 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
dev: true dev: true
/eslint-compat-utils@0.1.2(eslint@8.57.0): /eslint-compat-utils@0.5.0(eslint@8.57.0):
resolution: {integrity: sha512-Jia4JDldWnFNIru1Ehx1H5s9/yxiRHY/TimCuUc0jNexew3cF1gI6CYZil1ociakfWO3rRqFjl1mskBblB3RYg==} resolution: {integrity: sha512-dc6Y8tzEcSYZMHa+CMPLi/hyo1FzNeonbhJL7Ol0ccuKQkwopJcJBA9YL/xmMTLU1eKigXo9vj9nALElWYSowg==}
engines: {node: '>=12'} engines: {node: '>=12'}
peerDependencies: peerDependencies:
eslint: '>=6.0.0' eslint: '>=6.0.0'
dependencies: dependencies:
eslint: 8.57.0 eslint: 8.57.0
semver: 7.6.0
dev: true dev: true
/eslint-config-prettier@9.1.0(eslint@8.57.0): /eslint-config-prettier@9.1.0(eslint@8.57.0):
@ -4631,12 +4632,12 @@ packages:
eslint: 8.57.0 eslint: 8.57.0
dev: true dev: true
/eslint-plugin-svelte@2.35.1(eslint@8.57.0)(svelte@4.2.12)(ts-node@10.9.2): /eslint-plugin-svelte@2.36.0(eslint@8.57.0)(svelte@4.2.12)(ts-node@10.9.2):
resolution: {integrity: sha512-IF8TpLnROSGy98Z3NrsKXWDSCbNY2ReHDcrYTuXZMbfX7VmESISR78TWgO9zdg4Dht1X8coub5jKwHzP0ExRug==} resolution: {integrity: sha512-D30hSj13Y8YEn7yGXos7EYp0lpEb3Z2V/M+6a3MZ13KGVhaefdW2A9j8IBIcW4YR+j6fo901USzLeXQz/XbWeQ==}
engines: {node: ^14.17.0 || >=16.0.0} engines: {node: ^14.17.0 || >=16.0.0}
peerDependencies: peerDependencies:
eslint: ^7.0.0 || ^8.0.0-0 eslint: ^7.0.0 || ^8.0.0-0 || ^9.0.0-0
svelte: ^3.37.0 || ^4.0.0 svelte: ^3.37.0 || ^4.0.0 || ^5.0.0-next.95
peerDependenciesMeta: peerDependenciesMeta:
svelte: svelte:
optional: true optional: true
@ -4645,16 +4646,16 @@ packages:
'@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/sourcemap-codec': 1.4.15
debug: 4.3.4 debug: 4.3.4
eslint: 8.57.0 eslint: 8.57.0
eslint-compat-utils: 0.1.2(eslint@8.57.0) eslint-compat-utils: 0.5.0(eslint@8.57.0)
esutils: 2.0.3 esutils: 2.0.3
known-css-properties: 0.29.0 known-css-properties: 0.30.0
postcss: 8.4.38 postcss: 8.4.38
postcss-load-config: 3.1.4(postcss@8.4.38)(ts-node@10.9.2) postcss-load-config: 3.1.4(postcss@8.4.38)(ts-node@10.9.2)
postcss-safe-parser: 6.0.0(postcss@8.4.38) postcss-safe-parser: 6.0.0(postcss@8.4.38)
postcss-selector-parser: 6.0.13 postcss-selector-parser: 6.0.16
semver: 7.5.4 semver: 7.6.0
svelte: 4.2.12 svelte: 4.2.12
svelte-eslint-parser: 0.33.1(svelte@4.2.12) svelte-eslint-parser: 0.34.1(svelte@4.2.12)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
- ts-node - ts-node
@ -4898,7 +4899,7 @@ packages:
is-callable: 1.2.7 is-callable: 1.2.7
dev: false dev: false
/formsnap@0.5.1(svelte@4.2.12)(sveltekit-superforms@2.12.2): /formsnap@0.5.1(svelte@4.2.12)(sveltekit-superforms@2.12.4):
resolution: {integrity: sha512-8ppOlOu7llBEJbV0PzUz/KWh1J8KfiGqwjiyb8emQ2m+/nYXohLBtMcLVpW3XwlMkUbYaIXM+5lhfGjw8xbGJw==} resolution: {integrity: sha512-8ppOlOu7llBEJbV0PzUz/KWh1J8KfiGqwjiyb8emQ2m+/nYXohLBtMcLVpW3XwlMkUbYaIXM+5lhfGjw8xbGJw==}
peerDependencies: peerDependencies:
svelte: ^4.0.0 svelte: ^4.0.0
@ -4906,7 +4907,7 @@ packages:
dependencies: dependencies:
nanoid: 5.0.6 nanoid: 5.0.6
svelte: 4.2.12 svelte: 4.2.12
sveltekit-superforms: 2.12.2(@sveltejs/kit@2.5.5)(@types/json-schema@7.0.15)(esbuild-runner@2.2.2)(esbuild@0.20.2)(svelte@4.2.12) sveltekit-superforms: 2.12.4(@sveltejs/kit@2.5.5)(@types/json-schema@7.0.15)(esbuild-runner@2.2.2)(esbuild@0.20.2)(svelte@4.2.12)
dev: false dev: false
/fraction.js@4.3.7: /fraction.js@4.3.7:
@ -5399,8 +5400,8 @@ packages:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
/known-css-properties@0.29.0: /known-css-properties@0.30.0:
resolution: {integrity: sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==} resolution: {integrity: sha512-VSWXYUnsPu9+WYKkfmJyLKtIvaRJi1kXUqVmBACORXZQxT5oZDsoZ2vQP+bQFDnWtpI/4eq3MLoRMjI2fnLzTQ==}
dev: true dev: true
/levn@0.4.1: /levn@0.4.1:
@ -6334,7 +6335,7 @@ packages:
dependencies: dependencies:
lilconfig: 2.1.0 lilconfig: 2.1.0
postcss: 8.4.38 postcss: 8.4.38
ts-node: 10.9.2(@types/node@20.12.5)(typescript@5.4.4) ts-node: 10.9.2(@types/node@20.12.6)(typescript@5.4.4)
yaml: 1.10.2 yaml: 1.10.2
dev: true dev: true
@ -6352,7 +6353,7 @@ packages:
dependencies: dependencies:
lilconfig: 3.0.0 lilconfig: 3.0.0
postcss: 8.4.38 postcss: 8.4.38
ts-node: 10.9.2(@types/node@20.12.5)(typescript@5.4.4) ts-node: 10.9.2(@types/node@20.12.6)(typescript@5.4.4)
yaml: 2.3.4 yaml: 2.3.4
/postcss-load-config@5.0.3(postcss@8.4.38): /postcss-load-config@5.0.3(postcss@8.4.38):
@ -7163,11 +7164,11 @@ packages:
- sugarss - sugarss
dev: true dev: true
/svelte-eslint-parser@0.33.1(svelte@4.2.12): /svelte-eslint-parser@0.34.1(svelte@4.2.12):
resolution: {integrity: sha512-vo7xPGTlKBGdLH8T5L64FipvTrqv3OQRx9d2z5X05KKZDlF4rQk8KViZO4flKERY+5BiVdOh7zZ7JGJWo5P0uA==} resolution: {integrity: sha512-9+uLA1pqI9AZioKVGJzYYmlOZWxfoCXSbAM9iaNm7H01XlYlzRTtJfZgl9o3StQGN41PfGJIbkKkfk3e/pHFfA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies: peerDependencies:
svelte: ^3.37.0 || ^4.0.0 svelte: ^3.37.0 || ^4.0.0 || ^5.0.0-next.94
peerDependenciesMeta: peerDependenciesMeta:
svelte: svelte:
optional: true optional: true
@ -7220,8 +7221,8 @@ packages:
resolution: {integrity: sha512-AZD6R60vksyojn21FgXLglmBiBB9K5Dkdu0hdGrLbCaRCYT68IsWkZfRUqKhMx1IfzqWcZQ8X9y/f+Ih0oNQkQ==} resolution: {integrity: sha512-AZD6R60vksyojn21FgXLglmBiBB9K5Dkdu0hdGrLbCaRCYT68IsWkZfRUqKhMx1IfzqWcZQ8X9y/f+Ih0oNQkQ==}
dev: false dev: false
/svelte-meta-tags@3.1.1(svelte@4.2.12)(typescript@5.4.4): /svelte-meta-tags@3.1.2(svelte@4.2.12)(typescript@5.4.4):
resolution: {integrity: sha512-tSWU1xbRGV5rkDN4wWXZfY24BRuhPa3Z8W2Zpt3GCv01QQdWRhhgmlpYVdfoSPNQX060bDB/JM1cu4H+lXxe8w==} resolution: {integrity: sha512-zw8xSA10ce7atFO1o0N1x41+qU+HBnpGx8KcVRAWPy5iiRdO6fvUFMg6VwJVgMhLSBEUTZXKAvMALLUssbCoCw==}
peerDependencies: peerDependencies:
svelte: ^3.55.0 || ^4.0.0 svelte: ^3.55.0 || ^4.0.0
dependencies: dependencies:
@ -7360,8 +7361,8 @@ packages:
'@sveltejs/kit': 2.5.5(@sveltejs/vite-plugin-svelte@3.0.2)(svelte@4.2.12)(vite@5.2.8) '@sveltejs/kit': 2.5.5(@sveltejs/vite-plugin-svelte@3.0.2)(svelte@4.2.12)(vite@5.2.8)
dev: true dev: true
/sveltekit-superforms@2.12.2(@sveltejs/kit@2.5.5)(@types/json-schema@7.0.15)(esbuild-runner@2.2.2)(esbuild@0.20.2)(svelte@4.2.12): /sveltekit-superforms@2.12.4(@sveltejs/kit@2.5.5)(@types/json-schema@7.0.15)(esbuild-runner@2.2.2)(esbuild@0.20.2)(svelte@4.2.12):
resolution: {integrity: sha512-fFOXaluP1os/Tamx7gzwhT3tXPAfqZ8KYRC0UfXdXeUtlUIUfiGrIifDJ26/9uePmF8Zhqy2M0XjG8W9kQnJpg==} resolution: {integrity: sha512-0LILJfTpOZj8UhEfcuVZwuyDR63EGTjBq/TwHJn38PcC6KHGZ0bXu8DVHxslGUffEi9EHgeyshoJk7x3EoJVhQ==}
peerDependencies: peerDependencies:
'@sveltejs/kit': 1.x || 2.x '@sveltejs/kit': 1.x || 2.x
svelte: 3.x || 4.x || >=5.0.0-next.51 svelte: 3.x || 4.x || >=5.0.0-next.51
@ -7557,7 +7558,7 @@ packages:
/ts-interface-checker@0.1.13: /ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
/ts-node@10.9.2(@types/node@20.12.5)(typescript@5.4.4): /ts-node@10.9.2(@types/node@20.12.6)(typescript@5.4.4):
resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@ -7576,7 +7577,7 @@ packages:
'@tsconfig/node12': 1.0.11 '@tsconfig/node12': 1.0.11
'@tsconfig/node14': 1.0.3 '@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.4 '@tsconfig/node16': 1.0.4
'@types/node': 20.12.5 '@types/node': 20.12.6
acorn: 8.11.2 acorn: 8.11.2
acorn-walk: 8.3.0 acorn-walk: 8.3.0
arg: 4.1.3 arg: 4.1.3
@ -7725,7 +7726,7 @@ packages:
- rollup - rollup
dev: true dev: true
/vite-node@1.4.0(@types/node@20.12.5)(sass@1.74.1): /vite-node@1.4.0(@types/node@20.12.6)(sass@1.74.1):
resolution: {integrity: sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==} resolution: {integrity: sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==}
engines: {node: ^18.0.0 || >=20.0.0} engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true hasBin: true
@ -7734,7 +7735,7 @@ packages:
debug: 4.3.4 debug: 4.3.4
pathe: 1.1.2 pathe: 1.1.2
picocolors: 1.0.0 picocolors: 1.0.0
vite: 5.2.8(@types/node@20.12.5)(sass@1.74.1) vite: 5.2.8(@types/node@20.12.6)(sass@1.74.1)
transitivePeerDependencies: transitivePeerDependencies:
- '@types/node' - '@types/node'
- less - less
@ -7746,7 +7747,7 @@ packages:
- terser - terser
dev: true dev: true
/vite@5.2.8(@types/node@20.12.5)(sass@1.74.1): /vite@5.2.8(@types/node@20.12.6)(sass@1.74.1):
resolution: {integrity: sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==} resolution: {integrity: sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==}
engines: {node: ^18.0.0 || >=20.0.0} engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true hasBin: true
@ -7774,7 +7775,7 @@ packages:
terser: terser:
optional: true optional: true
dependencies: dependencies:
'@types/node': 20.12.5 '@types/node': 20.12.6
esbuild: 0.20.2 esbuild: 0.20.2
postcss: 8.4.38 postcss: 8.4.38
rollup: 4.13.0 rollup: 4.13.0
@ -7790,9 +7791,9 @@ packages:
vite: vite:
optional: true optional: true
dependencies: dependencies:
vite: 5.2.8(@types/node@20.12.5)(sass@1.74.1) vite: 5.2.8(@types/node@20.12.6)(sass@1.74.1)
/vitest@1.4.0(@types/node@20.12.5)(sass@1.74.1): /vitest@1.4.0(@types/node@20.12.6)(sass@1.74.1):
resolution: {integrity: sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==} resolution: {integrity: sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==}
engines: {node: ^18.0.0 || >=20.0.0} engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true hasBin: true
@ -7817,7 +7818,7 @@ packages:
jsdom: jsdom:
optional: true optional: true
dependencies: dependencies:
'@types/node': 20.12.5 '@types/node': 20.12.6
'@vitest/expect': 1.4.0 '@vitest/expect': 1.4.0
'@vitest/runner': 1.4.0 '@vitest/runner': 1.4.0
'@vitest/snapshot': 1.4.0 '@vitest/snapshot': 1.4.0
@ -7835,8 +7836,8 @@ packages:
strip-literal: 2.0.0 strip-literal: 2.0.0
tinybench: 2.6.0 tinybench: 2.6.0
tinypool: 0.8.2 tinypool: 0.8.2
vite: 5.2.8(@types/node@20.12.5)(sass@1.74.1) vite: 5.2.8(@types/node@20.12.6)(sass@1.74.1)
vite-node: 1.4.0(@types/node@20.12.5)(sass@1.74.1) vite-node: 1.4.0(@types/node@20.12.6)(sass@1.74.1)
why-is-node-running: 2.2.2 why-is-node-running: 2.2.2
transitivePeerDependencies: transitivePeerDependencies:
- less - less

View file

@ -4,18 +4,18 @@ import { userSchema } from './zod-schemas';
export const profileSchema = userSchema.pick({ export const profileSchema = userSchema.pick({
firstName: true, firstName: true,
lastName: true, lastName: true,
username: true username: true,
}); });
export const changeEmailSchema = userSchema.pick({ export const changeEmailSchema = userSchema.pick({
email: true email: true,
}); });
export const changeUserPasswordSchema = z export const changeUserPasswordSchema = z
.object({ .object({
current_password: z.string({ required_error: 'Current Password is required' }), current_password: z.string({ required_error: 'Current Password is required' }),
password: z.string({ required_error: 'Password is required' }).trim(), password: z.string({ required_error: 'Password is required' }).trim(),
confirm_password: z.string({ required_error: 'Confirm Password is required' }).trim() confirm_password: z.string({ required_error: 'Confirm Password is required' }).trim(),
}) })
.superRefine(({ confirm_password, password }, ctx) => { .superRefine(({ confirm_password, password }, ctx) => {
refinePasswords(confirm_password, password, ctx); refinePasswords(confirm_password, password, ctx);
@ -25,11 +25,17 @@ export type ChangeUserPasswordSchema = typeof changeUserPasswordSchema;
export const addTwoFactorSchema = z.object({ export const addTwoFactorSchema = z.object({
current_password: z.string({ required_error: 'Current Password is required' }), current_password: z.string({ required_error: 'Current Password is required' }),
two_factor_code: z.string({ required_error: 'Two Factor Code is required' }).trim() two_factor_code: z.string({ required_error: 'Two Factor Code is required' }).trim(),
}); });
export type AddTwoFactorSchema = typeof addTwoFactorSchema; export type AddTwoFactorSchema = typeof addTwoFactorSchema;
export const removeTwoFactorSchema = addTwoFactorSchema.pick({
current_password: true,
});
export type RemoveTwoFactorSchema = typeof removeTwoFactorSchema;
export const updateUserPasswordSchema = userSchema export const updateUserPasswordSchema = userSchema
.pick({ password: true, confirm_password: true }) .pick({ password: true, confirm_password: true })
.superRefine(({ confirm_password, password }, ctx) => { .superRefine(({ confirm_password, password }, ctx) => {
@ -39,7 +45,7 @@ export const updateUserPasswordSchema = userSchema
export const refinePasswords = async function ( export const refinePasswords = async function (
confirm_password: string, confirm_password: string,
password: string, password: string,
ctx: z.RefinementCtx ctx: z.RefinementCtx,
) { ) {
comparePasswords(confirm_password, password, ctx); comparePasswords(confirm_password, password, ctx);
checkPasswordStrength(password, ctx); checkPasswordStrength(password, ctx);
@ -48,13 +54,13 @@ export const refinePasswords = async function (
const comparePasswords = async function ( const comparePasswords = async function (
confirm_password: string, confirm_password: string,
password: string, password: string,
ctx: z.RefinementCtx ctx: z.RefinementCtx,
) { ) {
if (confirm_password !== password) { if (confirm_password !== password) {
ctx.addIssue({ ctx.addIssue({
code: 'custom', code: 'custom',
message: 'Password and Confirm Password must match', message: 'Password and Confirm Password must match',
path: ['confirm_password'] path: ['confirm_password'],
}); });
} }
}; };
@ -107,15 +113,15 @@ const checkPasswordStrength = async function (password: string, ctx: z.Refinemen
ctx.addIssue({ ctx.addIssue({
code: 'custom', code: 'custom',
message: errorMessage, message: errorMessage,
path: ['password'] path: ['password'],
}); });
} }
}; };
export const addRoleSchema = z.object({ export const addRoleSchema = z.object({
roles: z.array(z.string()).refine((value) => value.some((item) => item), { roles: z.array(z.string()).refine((value) => value.some((item) => item), {
message: 'You have to select at least one item.' message: 'You have to select at least one item.',
}) }),
}); });
export type AddRoleSchema = typeof addRoleSchema; export type AddRoleSchema = typeof addRoleSchema;

View file

@ -1,6 +1,6 @@
import { refinePasswords } from "./account"; import { refinePasswords } from './account';
import { userSchema } from "./zod-schemas"; import { userSchema } from './zod-schemas';
import {z} from "zod"; import { z } from 'zod';
export const signUpSchema = userSchema export const signUpSchema = userSchema
.pick({ .pick({
@ -10,7 +10,7 @@ export const signUpSchema = userSchema
username: true, username: true,
password: true, password: true,
confirm_password: true, confirm_password: true,
terms: true terms: true,
}) })
.superRefine(({ confirm_password, password }, ctx) => { .superRefine(({ confirm_password, password }, ctx) => {
refinePasswords(confirm_password, password, ctx); refinePasswords(confirm_password, password, ctx);
@ -18,12 +18,10 @@ export const signUpSchema = userSchema
export const signInSchema = z.object({ export const signInSchema = z.object({
username: z username: z
.string() .string()
.trim() .trim()
.min(3, { message: 'Username must be at least 3 characters' }) .min(3, { message: 'Username must be at least 3 characters' })
.max(50, { message: 'Username must be less than 50 characters' }), .max(50, { message: 'Username must be less than 50 characters' }),
password: z password: z.string({ required_error: 'Password is required' }).trim(),
.string({ required_error: 'Password is required' }) totpToken: z.string().trim().min(6).max(10).optional(),
.trim(),
totpToken: z.string().trim().min(6).max(6).optional()
}); });

View file

@ -94,11 +94,13 @@
</form> </form>
<div class="mt-6"> <div class="mt-6">
{#if !hasSetupTwoFactor} {#if !hasSetupTwoFactor}
<p>Two Factor Authentication is: <strong>Disabled</strong></p>
<Button variant="link" class="text-secondary-foreground" href="/profile/security/two-factor"> <Button variant="link" class="text-secondary-foreground" href="/profile/security/two-factor">
<KeyRound class="mr-2 h-4 w-4" /> <KeyRound class="mr-2 h-4 w-4" />
Setup 2FA Setup 2FA
</Button> </Button>
{:else} {:else}
<p>Two Factor Authentication is: <strong>Enabled</strong></p>
<Button variant="link" class="text-secondary-foreground" href="/profile/security/two-factor"> <Button variant="link" class="text-secondary-foreground" href="/profile/security/two-factor">
<KeyRound class="mr-2 h-4 w-4" /> <KeyRound class="mr-2 h-4 w-4" />
Disable 2FA Disable 2FA

View file

@ -7,21 +7,36 @@ import { HMAC } from 'oslo/crypto';
import QRCode from 'qrcode'; import QRCode from 'qrcode';
import { zod } from 'sveltekit-superforms/adapters'; import { zod } from 'sveltekit-superforms/adapters';
import { setError, superValidate } from 'sveltekit-superforms/server'; import { setError, superValidate } from 'sveltekit-superforms/server';
import { redirect } from 'sveltekit-flash-message/server'; import { redirect, setFlash } from 'sveltekit-flash-message/server';
import type { PageServerLoad } from '../../$types'; import type { PageServerLoad } from '../../$types';
import { addTwoFactorSchema } from '$lib/validations/account'; import { addTwoFactorSchema, removeTwoFactorSchema } from '$lib/validations/account';
import { notSignedInMessage } from '$lib/flashMessages'; import { notSignedInMessage } from '$lib/flashMessages';
import db from '$lib/drizzle'; import db from '$lib/drizzle';
import { users } from '../../../../../../schema'; import { recovery_codes, users } from '../../../../../../schema';
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
const form = await superValidate(event, zod(addTwoFactorSchema)); const addTwoFactorForm = await superValidate(event, zod(addTwoFactorSchema));
const removeTwoFactorForm = await superValidate(event, zod(removeTwoFactorSchema));
const user = event.locals.user; const user = event.locals.user;
if (!user) { if (!user) {
redirect(302, '/login', notSignedInMessage, event); redirect(302, '/login', notSignedInMessage, event);
} }
const dbUser = await db.query.users.findFirst({
where: eq(users.id, user.id),
});
if (dbUser?.two_factor_enabled) {
return {
addTwoFactorForm,
removeTwoFactorForm,
twoFactorEnabled: true,
recoveryCodes: [],
totpUri: '',
qrCode: '',
};
}
const twoFactorSecret = await new HMAC('SHA-1').generateKey(); const twoFactorSecret = await new HMAC('SHA-1').generateKey();
await db await db
.update(users) .update(users)
@ -36,12 +51,13 @@ export const load: PageServerLoad = async (event) => {
// pass the website's name and the user identifier (e.g. email, username) // pass the website's name and the user identifier (e.g. email, username)
const totpUri = createTOTPKeyURI(issuer, accountName, twoFactorSecret); const totpUri = createTOTPKeyURI(issuer, accountName, twoFactorSecret);
form.data = { addTwoFactorForm.data = {
current_password: '', current_password: '',
two_factor_code: '', two_factor_code: '',
}; };
return { return {
form, addTwoFactorForm,
removeTwoFactorForm,
twoFactorEnabled: false, twoFactorEnabled: false,
recoveryCodes: [], recoveryCodes: [],
totpUri, totpUri,
@ -50,12 +66,12 @@ export const load: PageServerLoad = async (event) => {
}; };
export const actions: Actions = { export const actions: Actions = {
default: async (event) => { enableTwoFactor: async (event) => {
const form = await superValidate(event, zod(addTwoFactorSchema)); const addTwoFactorForm = await superValidate(event, zod(addTwoFactorSchema));
if (!form.valid) { if (!addTwoFactorForm.valid) {
return fail(400, { return fail(400, {
form, addTwoFactorForm,
}); });
} }
@ -74,48 +90,102 @@ export const actions: Actions = {
}); });
if (!dbUser?.hashed_password) { if (!dbUser?.hashed_password) {
form.data.current_password = ''; addTwoFactorForm.data.current_password = '';
form.data.two_factor_code = ''; addTwoFactorForm.data.two_factor_code = '';
return setError( return setError(
form, addTwoFactorForm,
'Error occurred. Please try again or contact support if you need further help.', 'Error occurred. Please try again or contact support if you need further help.',
); );
} }
if (dbUser?.two_factor_secret === '' || dbUser?.two_factor_secret === null) { if (dbUser?.two_factor_secret === '' || dbUser?.two_factor_secret === null) {
form.data.current_password = ''; addTwoFactorForm.data.current_password = '';
form.data.two_factor_code = ''; addTwoFactorForm.data.two_factor_code = '';
return setError( return setError(
form, addTwoFactorForm,
'Error occurred. Please try again or contact support if you need further help.', 'Error occurred. Please try again or contact support if you need further help.',
); );
} }
const currentPasswordVerified = await new Argon2id().verify( const currentPasswordVerified = await new Argon2id().verify(
dbUser.hashed_password, dbUser.hashed_password,
form.data.current_password, addTwoFactorForm.data.current_password,
); );
if (!currentPasswordVerified) { if (!currentPasswordVerified) {
return setError(form, 'current_password', 'Your password is incorrect'); return setError(addTwoFactorForm, 'current_password', 'Your password is incorrect');
} }
if (form.data.two_factor_code === '') { if (addTwoFactorForm.data.two_factor_code === '') {
return setError(form, 'two_factor_code', 'Please enter a code'); return setError(addTwoFactorForm, 'two_factor_code', 'Please enter a code');
} }
const twoFactorCode = form.data.two_factor_code; const twoFactorCode = addTwoFactorForm.data.two_factor_code;
const validOTP = await new TOTPController().verify( const validOTP = await new TOTPController().verify(
twoFactorCode, twoFactorCode,
decodeHex(dbUser.two_factor_secret), decodeHex(dbUser.two_factor_secret),
); );
if (!validOTP) { if (!validOTP) {
return setError(form, 'two_factor_code', 'Invalid code'); return setError(addTwoFactorForm, 'two_factor_code', 'Invalid code');
} }
await db.update(users).set({ two_factor_enabled: true }).where(eq(users.id, user.id)); await db.update(users).set({ two_factor_enabled: true }).where(eq(users.id, user.id));
redirect(302, '/profile/security/two-factor/recovery-codes'); redirect(302, '/profile/security/two-factor/recovery-codes');
}, },
disableTwoFactor: async (event) => {
const { cookies } = event;
const removeTwoFactorForm = await superValidate(event, zod(removeTwoFactorSchema));
if (!removeTwoFactorForm.valid) {
return fail(400, {
removeTwoFactorForm,
});
}
if (!event.locals.user) {
redirect(302, '/login', notSignedInMessage, event);
}
if (!event.locals.session) {
return fail(401);
}
const user = event.locals.user;
const dbUser = await db.query.users.findFirst({
where: eq(users.id, user.id),
});
if (!dbUser?.hashed_password) {
removeTwoFactorForm.data.current_password = '';
return setError(
removeTwoFactorForm,
'Error occurred. Please try again or contact support if you need further help.',
);
}
const currentPasswordVerified = await new Argon2id().verify(
dbUser.hashed_password,
removeTwoFactorForm.data.current_password,
);
if (!currentPasswordVerified) {
return setError(removeTwoFactorForm, 'current_password', 'Your password is incorrect');
}
await db
.update(users)
.set({ two_factor_enabled: false, two_factor_secret: null })
.where(eq(users.id, user.id));
await db.delete(recovery_codes).where(eq(recovery_codes.userId, user.id));
setFlash({ type: 'success', message: 'Two-Factor Authentication has been disabled.' }, cookies);
return {
removeTwoFactorForm,
twoFactorEnabled: false,
recoveryCodes: [],
};
},
}; };

View file

@ -5,44 +5,64 @@
import * as Alert from '$components/ui/alert'; import * as Alert from '$components/ui/alert';
import * as Form from '$components/ui/form'; import * as Form from '$components/ui/form';
import { Input } from '$components/ui/input'; import { Input } from '$components/ui/input';
import { addTwoFactorSchema } from '$lib/validations/account'; import { addTwoFactorSchema, removeTwoFactorSchema } from '$lib/validations/account';
export let data; export let data;
const { qrCode, twoFactorEnabled, recoveryCodes } = data; const { qrCode, twoFactorEnabled, recoveryCodes } = data;
const form = superForm(data.form, { const addTwoFactorForm = superForm(data.addTwoFactorForm, {
taintedMessage: null, taintedMessage: null,
validators: zodClient(addTwoFactorSchema), validators: zodClient(addTwoFactorSchema),
delayMs: 500, delayMs: 500,
multipleSubmits: 'prevent', multipleSubmits: 'prevent',
}); });
const removeTwoFactorForm = superForm(data.removeTwoFactorForm, {
taintedMessage: null,
validators: zodClient(removeTwoFactorSchema),
delayMs: 500,
multipleSubmits: 'prevent',
});
console.log('Two Factor: ', twoFactorEnabled, recoveryCodes); console.log('Two Factor: ', twoFactorEnabled, recoveryCodes);
const { form: formData, enhance } = form; const { form: addTwoFactorFormData, enhance: addTwoFactorEnhance } = addTwoFactorForm;
const { form: removeTwoFactorFormData, enhance: removeTwoFactorEnhance } = removeTwoFactorForm;
</script> </script>
<h1>Two-Factor Authentication</h1> <h1>Two-Factor Authentication</h1>
{#if twoFactorEnabled} {#if twoFactorEnabled}
<h2>Two-Factor Authentication is <span class="text-green-500">enabled</span></h2> <h2>Currently you have two factor authentication <span class="text-green-500">enabled</span></h2>
<p>To disable two factor authentication, please enter your current password.</p>
<form method="POST" action="?/disableTwoFactor" use:removeTwoFactorEnhance data-sveltekit-replacestate>
<Form.Field form={removeTwoFactorForm} name="current_password">
<Form.Control let:attrs>
<Form.Label for="password">Current Password</Form.Label>
<Input type="password" {...attrs} bind:value={$removeTwoFactorFormData.current_password} />
</Form.Control>
<Form.Description>Please enter your current password.</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Button>Disable Two Factor Authentication</Form.Button>
</form>
{:else} {:else}
<h2>Please scan the following QR Code</h2> <h2>Please scan the following QR Code</h2>
<img src={qrCode} alt="QR Code" /> <img src={qrCode} alt="QR Code" />
<form method="POST" use:enhance data-sveltekit-replacestate> <form method="POST" action="?/enableTwoFactor" use:addTwoFactorEnhance data-sveltekit-replacestate>
<Form.Field {form} name="two_factor_code"> <Form.Field form={addTwoFactorForm} name="two_factor_code">
<Form.Control let:attrs> <Form.Control let:attrs>
<Form.Label for="code">Enter Code</Form.Label> <Form.Label for="code">Enter Code</Form.Label>
<Input {...attrs} bind:value={$formData.two_factor_code} /> <Input {...attrs} bind:value={$addTwoFactorFormData.two_factor_code} />
</Form.Control> </Form.Control>
<Form.Description>This is the code from your authenticator app.</Form.Description> <Form.Description>This is the code from your authenticator app.</Form.Description>
<Form.FieldErrors /> <Form.FieldErrors />
</Form.Field> </Form.Field>
<Form.Field {form} name="current_password"> <Form.Field form={addTwoFactorForm} name="current_password">
<Form.Control let:attrs> <Form.Control let:attrs>
<Form.Label for="password">Enter Password</Form.Label> <Form.Label for="password">Enter Password</Form.Label>
<Input type="password" {...attrs} bind:value={$formData.current_password} /> <Input type="password" {...attrs} bind:value={$addTwoFactorFormData.current_password} />
</Form.Control> </Form.Control>
<Form.Description>Please enter your current password.</Form.Description> <Form.Description>Please enter your current password.</Form.Description>
<Form.FieldErrors /> <Form.FieldErrors />

View file

@ -1,11 +1,11 @@
import db from "$lib/drizzle"; import db from '$lib/drizzle';
import {eq} from "drizzle-orm"; import { eq } from 'drizzle-orm';
import {Argon2id} from "oslo/password"; import { Argon2id } from 'oslo/password';
import {alphabet, generateRandomString} from "oslo/crypto"; import { alphabet, generateRandomString } from 'oslo/crypto';
import {redirect} from "sveltekit-flash-message/server"; import { redirect } from 'sveltekit-flash-message/server';
import {notSignedInMessage} from "$lib/flashMessages"; import { notSignedInMessage } from '$lib/flashMessages';
import type { PageServerLoad } from '../../../$types'; import type { PageServerLoad } from '../../../$types';
import {recovery_codes, users} from "../../../../../../../schema"; import { recovery_codes, users } from '../../../../../../../schema';
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
const user = event.locals.user; const user = event.locals.user;
@ -24,12 +24,16 @@ export const load: PageServerLoad = async (event) => {
}); });
if (recoveryCodes.length === 0) { if (recoveryCodes.length === 0) {
const recoveryCodes = Array.from({length: 5}, () => generateRandomString(10, alphabet('A-Z', '0-9'))); const recoveryCodes = Array.from({ length: 5 }, () =>
generateRandomString(10, alphabet('A-Z', '0-9')),
);
if (recoveryCodes) { if (recoveryCodes) {
for (const code of recoveryCodes) { for (const code of recoveryCodes) {
const hashedCode = await new Argon2id().hash(code);
console.log('Inserting recovery code', code, hashedCode);
await db.insert(recovery_codes).values({ await db.insert(recovery_codes).values({
userId: user.id, userId: user.id,
code: await new Argon2id().hash(code), code: hashedCode,
}); });
} }
} }
@ -39,8 +43,13 @@ export const load: PageServerLoad = async (event) => {
} }
return { return {
recoveryCodes: [], recoveryCodes: [],
} };
} else { } else {
redirect(302, '/profile', { message: 'Two-Factor Authentication is not enabled', type: 'error' }, event); redirect(
302,
'/profile',
{ message: 'Two-Factor Authentication is not enabled', type: 'error' },
event,
);
} }
} };

View file

@ -1,5 +1,5 @@
import { fail, error, type Actions } from '@sveltejs/kit'; import { fail, error, type Actions } from '@sveltejs/kit';
import { eq } from 'drizzle-orm'; import { and, eq, ne } from 'drizzle-orm';
import { Argon2id } from 'oslo/password'; import { Argon2id } from 'oslo/password';
import { decodeHex } from 'oslo/encoding'; import { decodeHex } from 'oslo/encoding';
import { TOTPController } from 'oslo/otp'; import { TOTPController } from 'oslo/otp';
@ -10,7 +10,7 @@ import { RateLimiter } from 'sveltekit-rate-limiter/server';
import db from '$lib/drizzle'; import db from '$lib/drizzle';
import { lucia } from '$lib/server/auth'; import { lucia } from '$lib/server/auth';
import { signInSchema } from '$lib/validations/auth'; import { signInSchema } from '$lib/validations/auth';
import { collections, users, wishlists } from '../../../schema'; import { users, recovery_codes } from '../../../schema';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
@ -70,18 +70,18 @@ export const actions: Actions = {
return setError(form, '', 'Your username or password is incorrect.'); return setError(form, '', 'Your username or password is incorrect.');
} }
await db // await db
.insert(collections) // .insert(collections)
.values({ // .values({
user_id: user.id, // user_id: user.id,
}) // })
.onConflictDoNothing(); // .onConflictDoNothing();
await db // await db
.insert(wishlists) // .insert(wishlists)
.values({ // .values({
user_id: user.id, // user_id: user.id,
}) // })
.onConflictDoNothing(); // .onConflictDoNothing();
if (user?.two_factor_enabled && user?.two_factor_secret && !form?.data?.totpToken) { if (user?.two_factor_enabled && user?.two_factor_secret && !form?.data?.totpToken) {
return fail(400, { return fail(400, {
@ -92,15 +92,21 @@ export const actions: Actions = {
console.log('totpToken', form.data.totpToken); console.log('totpToken', form.data.totpToken);
const validOTP = await new TOTPController().verify( const validOTP = await new TOTPController().verify(
form.data.totpToken, form.data.totpToken,
decodeHex(user.two_factor_secret) decodeHex(user.two_factor_secret),
); );
console.log('validOTP', validOTP); console.log('validOTP', validOTP);
form.errors.totpToken = ['Invalid TOTP code'];
if (!validOTP) { if (!validOTP) {
return fail(400, { console.log('invalid TOTP code check for recovery codes');
form, const usedRecoveryCode = await checkRecoveryCode(form?.data?.totpToken, user.id);
twoFactorRequired: true, if (!usedRecoveryCode) {
}); console.log('invalid TOTP code');
form.errors.totpToken = ['Invalid code'];
return fail(400, {
form,
twoFactorRequired: true,
});
}
} }
} }
console.log('ip', locals.ip); console.log('ip', locals.ip);
@ -131,3 +137,22 @@ export const actions: Actions = {
redirect(302, '/', message, event); redirect(302, '/', message, event);
}, },
}; };
async function checkRecoveryCode(recoveryCode: string, userId: string) {
const userRecoveryCodes = await db.query.recovery_codes.findMany({
where: and(eq(recovery_codes.used, false), eq(recovery_codes.userId, userId)),
});
for (const code of userRecoveryCodes) {
const validRecoveryCode = await new Argon2id().verify(code.code, recoveryCode);
if (validRecoveryCode) {
await db
.update(recovery_codes)
.set({
used: true,
})
.where(eq(recovery_codes.id, code.id));
return true;
}
}
return false;
}

View file

@ -65,7 +65,7 @@
{#if form?.twoFactorRequired} {#if form?.twoFactorRequired}
<Form.Field form={superLoginForm} name="totpToken"> <Form.Field form={superLoginForm} name="totpToken">
<Form.Control let:attrs> <Form.Control let:attrs>
<Form.Label for="totpToken">2FA Code</Form.Label> <Form.Label for="totpToken">Two Factor Code or Recovery Code</Form.Label>
<Input {...attrs} autocomplete="one-time-code" bind:value={$loginForm.totpToken} /> <Input {...attrs} autocomplete="one-time-code" bind:value={$loginForm.totpToken} />
</Form.Control> </Form.Control>
<Form.FieldErrors /> <Form.FieldErrors />

View file

@ -45,6 +45,8 @@ export const recovery_codes = pgTable('recovery_codes', {
updated_at: timestamp('updated_at').notNull().defaultNow(), updated_at: timestamp('updated_at').notNull().defaultNow(),
}); });
export type RecoveryCodes = InferSelectModel<typeof recovery_codes>;
export const user_relations = relations(users, ({ many }) => ({ export const user_relations = relations(users, ({ many }) => ({
user_roles: many(user_roles), user_roles: many(user_roles),
})); }));