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/vite-plugin-svelte": "^3.0.2",
"@types/cookie": "^0.6.0",
"@types/node": "^20.12.5",
"@types/pg": "^8.11.4",
"@types/node": "^20.12.6",
"@types/pg": "^8.11.5",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"autoprefixer": "^10.4.19",
@ -39,7 +39,7 @@
"drizzle-kit": "^0.20.14",
"eslint": "^8.57.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-debounce-it": "^3.2.0",
"postcss": "^8.4.38",
@ -54,12 +54,12 @@
"svelte": "^4.2.12",
"svelte-check": "^3.6.9",
"svelte-headless-table": "^0.18.2",
"svelte-meta-tags": "^3.1.1",
"svelte-meta-tags": "^3.1.2",
"svelte-preprocess": "^5.1.3",
"svelte-sequential-preprocessor": "^2.0.1",
"sveltekit-flash-message": "^2.4.4",
"sveltekit-rate-limiter": "^0.4.3",
"sveltekit-superforms": "^2.12.2",
"sveltekit-superforms": "^2.12.4",
"tailwindcss": "^3.4.3",
"ts-node": "^10.9.2",
"tslib": "^2.6.1",

View file

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

View file

@ -4,18 +4,18 @@ import { userSchema } from './zod-schemas';
export const profileSchema = userSchema.pick({
firstName: true,
lastName: true,
username: true
username: true,
});
export const changeEmailSchema = userSchema.pick({
email: true
email: true,
});
export const changeUserPasswordSchema = z
.object({
current_password: z.string({ required_error: 'Current Password is required' }),
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) => {
refinePasswords(confirm_password, password, ctx);
@ -25,11 +25,17 @@ export type ChangeUserPasswordSchema = typeof changeUserPasswordSchema;
export const addTwoFactorSchema = z.object({
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 const removeTwoFactorSchema = addTwoFactorSchema.pick({
current_password: true,
});
export type RemoveTwoFactorSchema = typeof removeTwoFactorSchema;
export const updateUserPasswordSchema = userSchema
.pick({ password: true, confirm_password: true })
.superRefine(({ confirm_password, password }, ctx) => {
@ -39,7 +45,7 @@ export const updateUserPasswordSchema = userSchema
export const refinePasswords = async function (
confirm_password: string,
password: string,
ctx: z.RefinementCtx
ctx: z.RefinementCtx,
) {
comparePasswords(confirm_password, password, ctx);
checkPasswordStrength(password, ctx);
@ -48,13 +54,13 @@ export const refinePasswords = async function (
const comparePasswords = async function (
confirm_password: string,
password: string,
ctx: z.RefinementCtx
ctx: z.RefinementCtx,
) {
if (confirm_password !== password) {
ctx.addIssue({
code: 'custom',
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({
code: 'custom',
message: errorMessage,
path: ['password']
path: ['password'],
});
}
};
export const addRoleSchema = z.object({
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;

View file

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

View file

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

View file

@ -7,21 +7,36 @@ import { HMAC } from 'oslo/crypto';
import QRCode from 'qrcode';
import { zod } from 'sveltekit-superforms/adapters';
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 { addTwoFactorSchema } from '$lib/validations/account';
import { addTwoFactorSchema, removeTwoFactorSchema } from '$lib/validations/account';
import { notSignedInMessage } from '$lib/flashMessages';
import db from '$lib/drizzle';
import { users } from '../../../../../../schema';
import { recovery_codes, users } from '../../../../../../schema';
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;
if (!user) {
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();
await db
.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)
const totpUri = createTOTPKeyURI(issuer, accountName, twoFactorSecret);
form.data = {
addTwoFactorForm.data = {
current_password: '',
two_factor_code: '',
};
return {
form,
addTwoFactorForm,
removeTwoFactorForm,
twoFactorEnabled: false,
recoveryCodes: [],
totpUri,
@ -50,12 +66,12 @@ export const load: PageServerLoad = async (event) => {
};
export const actions: Actions = {
default: async (event) => {
const form = await superValidate(event, zod(addTwoFactorSchema));
enableTwoFactor: async (event) => {
const addTwoFactorForm = await superValidate(event, zod(addTwoFactorSchema));
if (!form.valid) {
if (!addTwoFactorForm.valid) {
return fail(400, {
form,
addTwoFactorForm,
});
}
@ -74,48 +90,102 @@ export const actions: Actions = {
});
if (!dbUser?.hashed_password) {
form.data.current_password = '';
form.data.two_factor_code = '';
addTwoFactorForm.data.current_password = '';
addTwoFactorForm.data.two_factor_code = '';
return setError(
form,
addTwoFactorForm,
'Error occurred. Please try again or contact support if you need further help.',
);
}
if (dbUser?.two_factor_secret === '' || dbUser?.two_factor_secret === null) {
form.data.current_password = '';
form.data.two_factor_code = '';
addTwoFactorForm.data.current_password = '';
addTwoFactorForm.data.two_factor_code = '';
return setError(
form,
addTwoFactorForm,
'Error occurred. Please try again or contact support if you need further help.',
);
}
const currentPasswordVerified = await new Argon2id().verify(
dbUser.hashed_password,
form.data.current_password,
addTwoFactorForm.data.current_password,
);
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 === '') {
return setError(form, 'two_factor_code', 'Please enter a code');
if (addTwoFactorForm.data.two_factor_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(
twoFactorCode,
decodeHex(dbUser.two_factor_secret),
);
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));
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 Form from '$components/ui/form';
import { Input } from '$components/ui/input';
import { addTwoFactorSchema } from '$lib/validations/account';
import { addTwoFactorSchema, removeTwoFactorSchema } from '$lib/validations/account';
export let data;
const { qrCode, twoFactorEnabled, recoveryCodes } = data;
const form = superForm(data.form, {
const addTwoFactorForm = superForm(data.addTwoFactorForm, {
taintedMessage: null,
validators: zodClient(addTwoFactorSchema),
delayMs: 500,
multipleSubmits: 'prevent',
});
const removeTwoFactorForm = superForm(data.removeTwoFactorForm, {
taintedMessage: null,
validators: zodClient(removeTwoFactorSchema),
delayMs: 500,
multipleSubmits: 'prevent',
});
console.log('Two Factor: ', twoFactorEnabled, recoveryCodes);
const { form: formData, enhance } = form;
const { form: addTwoFactorFormData, enhance: addTwoFactorEnhance } = addTwoFactorForm;
const { form: removeTwoFactorFormData, enhance: removeTwoFactorEnhance } = removeTwoFactorForm;
</script>
<h1>Two-Factor Authentication</h1>
{#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}
<h2>Please scan the following QR Code</h2>
<img src={qrCode} alt="QR Code" />
<form method="POST" use:enhance data-sveltekit-replacestate>
<Form.Field {form} name="two_factor_code">
<form method="POST" action="?/enableTwoFactor" use:addTwoFactorEnhance data-sveltekit-replacestate>
<Form.Field form={addTwoFactorForm} name="two_factor_code">
<Form.Control let:attrs>
<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.Description>This is the code from your authenticator app.</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="current_password">
<Form.Field form={addTwoFactorForm} name="current_password">
<Form.Control let:attrs>
<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.Description>Please enter your current password.</Form.Description>
<Form.FieldErrors />

View file

@ -1,11 +1,11 @@
import db from "$lib/drizzle";
import {eq} from "drizzle-orm";
import {Argon2id} from "oslo/password";
import {alphabet, generateRandomString} from "oslo/crypto";
import {redirect} from "sveltekit-flash-message/server";
import {notSignedInMessage} from "$lib/flashMessages";
import db from '$lib/drizzle';
import { eq } from 'drizzle-orm';
import { Argon2id } from 'oslo/password';
import { alphabet, generateRandomString } from 'oslo/crypto';
import { redirect } from 'sveltekit-flash-message/server';
import { notSignedInMessage } from '$lib/flashMessages';
import type { PageServerLoad } from '../../../$types';
import {recovery_codes, users} from "../../../../../../../schema";
import { recovery_codes, users } from '../../../../../../../schema';
export const load: PageServerLoad = async (event) => {
const user = event.locals.user;
@ -24,12 +24,16 @@ export const load: PageServerLoad = async (event) => {
});
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) {
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({
userId: user.id,
code: await new Argon2id().hash(code),
code: hashedCode,
});
}
}
@ -39,8 +43,13 @@ export const load: PageServerLoad = async (event) => {
}
return {
recoveryCodes: [],
}
};
} 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 { eq } from 'drizzle-orm';
import { and, eq, ne } from 'drizzle-orm';
import { Argon2id } from 'oslo/password';
import { decodeHex } from 'oslo/encoding';
import { TOTPController } from 'oslo/otp';
@ -10,7 +10,7 @@ import { RateLimiter } from 'sveltekit-rate-limiter/server';
import db from '$lib/drizzle';
import { lucia } from '$lib/server/auth';
import { signInSchema } from '$lib/validations/auth';
import { collections, users, wishlists } from '../../../schema';
import { users, recovery_codes } from '../../../schema';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => {
@ -70,18 +70,18 @@ export const actions: Actions = {
return setError(form, '', 'Your username or password is incorrect.');
}
await db
.insert(collections)
.values({
user_id: user.id,
})
.onConflictDoNothing();
await db
.insert(wishlists)
.values({
user_id: user.id,
})
.onConflictDoNothing();
// await db
// .insert(collections)
// .values({
// user_id: user.id,
// })
// .onConflictDoNothing();
// await db
// .insert(wishlists)
// .values({
// user_id: user.id,
// })
// .onConflictDoNothing();
if (user?.two_factor_enabled && user?.two_factor_secret && !form?.data?.totpToken) {
return fail(400, {
@ -92,15 +92,21 @@ export const actions: Actions = {
console.log('totpToken', form.data.totpToken);
const validOTP = await new TOTPController().verify(
form.data.totpToken,
decodeHex(user.two_factor_secret)
decodeHex(user.two_factor_secret),
);
console.log('validOTP', validOTP);
form.errors.totpToken = ['Invalid TOTP code'];
if (!validOTP) {
return fail(400, {
form,
twoFactorRequired: true,
});
console.log('invalid TOTP code check for recovery codes');
const usedRecoveryCode = await checkRecoveryCode(form?.data?.totpToken, user.id);
if (!usedRecoveryCode) {
console.log('invalid TOTP code');
form.errors.totpToken = ['Invalid code'];
return fail(400, {
form,
twoFactorRequired: true,
});
}
}
}
console.log('ip', locals.ip);
@ -131,3 +137,22 @@ export const actions: Actions = {
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}
<Form.Field form={superLoginForm} name="totpToken">
<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} />
</Form.Control>
<Form.FieldErrors />

View file

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