mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
Encrypting two factor secret in the DB, adding env for secret used to encrypt, and creating/verifying totp codes.
This commit is contained in:
parent
3204b0b28b
commit
6e67b2d4e1
29 changed files with 915 additions and 869 deletions
|
|
@ -10,6 +10,7 @@ DATABASE_PASSWORD='postgres'
|
||||||
DATABASE_HOST='localhost'
|
DATABASE_HOST='localhost'
|
||||||
DATABASE_PORT=5432
|
DATABASE_PORT=5432
|
||||||
DATABASE_DB='postgres'
|
DATABASE_DB='postgres'
|
||||||
|
ENCRYPTION_KEY=""
|
||||||
|
|
||||||
REDIS_URL='redis://127.0.0.1:6379/0'
|
REDIS_URL='redis://127.0.0.1:6379/0'
|
||||||
|
|
||||||
|
|
|
||||||
5
.vscode/extensions.json
vendored
Normal file
5
.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"inlang.vs-code-extension"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
"linter": { "enabled": true, "rules": { "recommended": true } },
|
"linter": { "enabled": true, "rules": { "recommended": true } },
|
||||||
"javascript": {
|
"javascript": {
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"jsxQuoteStyle": "double",
|
"jsxQuoteStyle": "single",
|
||||||
"quoteProperties": "asNeeded",
|
"quoteProperties": "asNeeded",
|
||||||
"trailingCommas": "all",
|
"trailingCommas": "all",
|
||||||
"indentStyle": "space",
|
"indentStyle": "space",
|
||||||
|
|
|
||||||
11
package.json
11
package.json
|
|
@ -27,7 +27,7 @@
|
||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^8.4.1",
|
||||||
"@melt-ui/pp": "^0.3.2",
|
"@melt-ui/pp": "^0.3.2",
|
||||||
"@melt-ui/svelte": "^0.83.0",
|
"@melt-ui/svelte": "^0.83.0",
|
||||||
"@playwright/test": "^1.48.2",
|
"@playwright/test": "^1.49.0",
|
||||||
"@sveltejs/adapter-auto": "^3.3.1",
|
"@sveltejs/adapter-auto": "^3.3.1",
|
||||||
"@sveltejs/enhanced-img": "^0.3.10",
|
"@sveltejs/enhanced-img": "^0.3.10",
|
||||||
"@sveltejs/kit": "^2.8.1",
|
"@sveltejs/kit": "^2.8.1",
|
||||||
|
|
@ -63,8 +63,8 @@
|
||||||
"svelte-sequential-preprocessor": "^2.0.2",
|
"svelte-sequential-preprocessor": "^2.0.2",
|
||||||
"svelte-sonner": "^0.3.28",
|
"svelte-sonner": "^0.3.28",
|
||||||
"sveltekit-flash-message": "^2.4.4",
|
"sveltekit-flash-message": "^2.4.4",
|
||||||
"sveltekit-superforms": "^2.20.0",
|
"sveltekit-superforms": "^2.20.1",
|
||||||
"tailwindcss": "^3.4.14",
|
"tailwindcss": "^3.4.15",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"tsx": "^4.19.2",
|
"tsx": "^4.19.2",
|
||||||
|
|
@ -88,6 +88,7 @@
|
||||||
"@needle-di/core": "^0.8.4",
|
"@needle-di/core": "^0.8.4",
|
||||||
"@neondatabase/serverless": "^0.9.5",
|
"@neondatabase/serverless": "^0.9.5",
|
||||||
"@node-rs/argon2": "^1.8.3",
|
"@node-rs/argon2": "^1.8.3",
|
||||||
|
"@oslojs/binary": "^1.0.0",
|
||||||
"@oslojs/crypto": "^1.0.1",
|
"@oslojs/crypto": "^1.0.1",
|
||||||
"@oslojs/encoding": "^1.1.0",
|
"@oslojs/encoding": "^1.1.0",
|
||||||
"@oslojs/jwt": "^0.2.0",
|
"@oslojs/jwt": "^0.2.0",
|
||||||
|
|
@ -100,13 +101,13 @@
|
||||||
"@sveltejs/adapter-vercel": "^5.4.7",
|
"@sveltejs/adapter-vercel": "^5.4.7",
|
||||||
"@types/feather-icons": "^4.29.4",
|
"@types/feather-icons": "^4.29.4",
|
||||||
"boardgamegeekclient": "^1.9.1",
|
"boardgamegeekclient": "^1.9.1",
|
||||||
"bullmq": "^5.25.6",
|
"bullmq": "^5.27.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cookie": "^1.0.1",
|
"cookie": "^1.0.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"dotenv-expand": "^11.0.7",
|
"dotenv-expand": "^11.0.7",
|
||||||
"drizzle-orm": "^0.36.1",
|
"drizzle-orm": "^0.36.3",
|
||||||
"drizzle-zod": "^0.5.1",
|
"drizzle-zod": "^0.5.1",
|
||||||
"feather-icons": "^4.29.2",
|
"feather-icons": "^4.29.2",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
|
|
|
||||||
194
pnpm-lock.yaml
194
pnpm-lock.yaml
|
|
@ -34,7 +34,7 @@ importers:
|
||||||
version: 3.5.6
|
version: 3.5.6
|
||||||
'@lucia-auth/adapter-drizzle':
|
'@lucia-auth/adapter-drizzle':
|
||||||
specifier: ^1.1.0
|
specifier: ^1.1.0
|
||||||
version: 1.1.0(drizzle-orm@0.36.1(@neondatabase/serverless@0.9.5)(@types/pg@8.11.10)(pg@8.13.1)(postgres@3.4.5))(lucia@3.2.0)
|
version: 1.1.0(drizzle-orm@0.36.3(@neondatabase/serverless@0.9.5)(@types/pg@8.11.10)(pg@8.13.1)(postgres@3.4.5))(lucia@3.2.0)
|
||||||
'@lukeed/uuid':
|
'@lukeed/uuid':
|
||||||
specifier: ^2.0.1
|
specifier: ^2.0.1
|
||||||
version: 2.0.1
|
version: 2.0.1
|
||||||
|
|
@ -47,6 +47,9 @@ importers:
|
||||||
'@node-rs/argon2':
|
'@node-rs/argon2':
|
||||||
specifier: ^1.8.3
|
specifier: ^1.8.3
|
||||||
version: 1.8.3
|
version: 1.8.3
|
||||||
|
'@oslojs/binary':
|
||||||
|
specifier: ^1.0.0
|
||||||
|
version: 1.0.0
|
||||||
'@oslojs/crypto':
|
'@oslojs/crypto':
|
||||||
specifier: ^1.0.1
|
specifier: ^1.0.1
|
||||||
version: 1.0.1
|
version: 1.0.1
|
||||||
|
|
@ -84,8 +87,8 @@ importers:
|
||||||
specifier: ^1.9.1
|
specifier: ^1.9.1
|
||||||
version: 1.9.1
|
version: 1.9.1
|
||||||
bullmq:
|
bullmq:
|
||||||
specifier: ^5.25.6
|
specifier: ^5.27.0
|
||||||
version: 5.25.6
|
version: 5.27.0
|
||||||
class-variance-authority:
|
class-variance-authority:
|
||||||
specifier: ^0.7.0
|
specifier: ^0.7.0
|
||||||
version: 0.7.0
|
version: 0.7.0
|
||||||
|
|
@ -102,11 +105,11 @@ importers:
|
||||||
specifier: ^11.0.7
|
specifier: ^11.0.7
|
||||||
version: 11.0.7
|
version: 11.0.7
|
||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: ^0.36.1
|
specifier: ^0.36.3
|
||||||
version: 0.36.1(@neondatabase/serverless@0.9.5)(@types/pg@8.11.10)(pg@8.13.1)(postgres@3.4.5)
|
version: 0.36.3(@neondatabase/serverless@0.9.5)(@types/pg@8.11.10)(pg@8.13.1)(postgres@3.4.5)
|
||||||
drizzle-zod:
|
drizzle-zod:
|
||||||
specifier: ^0.5.1
|
specifier: ^0.5.1
|
||||||
version: 0.5.1(drizzle-orm@0.36.1(@neondatabase/serverless@0.9.5)(@types/pg@8.11.10)(pg@8.13.1)(postgres@3.4.5))(zod@3.23.8)
|
version: 0.5.1(drizzle-orm@0.36.3(@neondatabase/serverless@0.9.5)(@types/pg@8.11.10)(pg@8.13.1)(postgres@3.4.5))(zod@3.23.8)
|
||||||
feather-icons:
|
feather-icons:
|
||||||
specifier: ^4.29.2
|
specifier: ^4.29.2
|
||||||
version: 4.29.2
|
version: 4.29.2
|
||||||
|
|
@ -184,10 +187,10 @@ importers:
|
||||||
version: 2.5.4
|
version: 2.5.4
|
||||||
tailwind-variants:
|
tailwind-variants:
|
||||||
specifier: ^0.2.1
|
specifier: ^0.2.1
|
||||||
version: 0.2.1(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)))
|
version: 0.2.1(tailwindcss@3.4.15(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)))
|
||||||
tailwindcss-animate:
|
tailwindcss-animate:
|
||||||
specifier: ^1.0.7
|
specifier: ^1.0.7
|
||||||
version: 1.0.7(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)))
|
version: 1.0.7(tailwindcss@3.4.15(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)))
|
||||||
tsyringe:
|
tsyringe:
|
||||||
specifier: ^4.8.0
|
specifier: ^4.8.0
|
||||||
version: 4.8.0
|
version: 4.8.0
|
||||||
|
|
@ -208,8 +211,8 @@ importers:
|
||||||
specifier: ^0.83.0
|
specifier: ^0.83.0
|
||||||
version: 0.83.0(svelte@5.0.0-next.175)
|
version: 0.83.0(svelte@5.0.0-next.175)
|
||||||
'@playwright/test':
|
'@playwright/test':
|
||||||
specifier: ^1.48.2
|
specifier: ^1.49.0
|
||||||
version: 1.48.2
|
version: 1.49.0
|
||||||
'@sveltejs/adapter-auto':
|
'@sveltejs/adapter-auto':
|
||||||
specifier: ^3.3.1
|
specifier: ^3.3.1
|
||||||
version: 3.3.1(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))
|
version: 3.3.1(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))
|
||||||
|
|
@ -254,7 +257,7 @@ importers:
|
||||||
version: 0.27.2
|
version: 0.27.2
|
||||||
formsnap:
|
formsnap:
|
||||||
specifier: ^1.0.1
|
specifier: ^1.0.1
|
||||||
version: 1.0.1(svelte@5.0.0-next.175)(sveltekit-superforms@2.20.0(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(@types/json-schema@7.0.15)(svelte@5.0.0-next.175)(typescript@5.6.3))
|
version: 1.0.1(svelte@5.0.0-next.175)(sveltekit-superforms@2.20.1(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(@types/json-schema@7.0.15)(svelte@5.0.0-next.175)(typescript@5.6.3))
|
||||||
just-clone:
|
just-clone:
|
||||||
specifier: ^6.2.0
|
specifier: ^6.2.0
|
||||||
version: 6.2.0
|
version: 6.2.0
|
||||||
|
|
@ -316,11 +319,11 @@ importers:
|
||||||
specifier: ^2.4.4
|
specifier: ^2.4.4
|
||||||
version: 2.4.4(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(svelte@5.0.0-next.175)
|
version: 2.4.4(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(svelte@5.0.0-next.175)
|
||||||
sveltekit-superforms:
|
sveltekit-superforms:
|
||||||
specifier: ^2.20.0
|
specifier: ^2.20.1
|
||||||
version: 2.20.0(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(@types/json-schema@7.0.15)(svelte@5.0.0-next.175)(typescript@5.6.3)
|
version: 2.20.1(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(@types/json-schema@7.0.15)(svelte@5.0.0-next.175)(typescript@5.6.3)
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^3.4.14
|
specifier: ^3.4.15
|
||||||
version: 3.4.14(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3))
|
version: 3.4.15(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3))
|
||||||
ts-node:
|
ts-node:
|
||||||
specifier: ^10.9.2
|
specifier: ^10.9.2
|
||||||
version: 10.9.2(@types/node@20.17.6)(typescript@5.6.3)
|
version: 10.9.2(@types/node@20.17.6)(typescript@5.6.3)
|
||||||
|
|
@ -353,11 +356,11 @@ packages:
|
||||||
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
|
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
'@ark/schema@0.10.0':
|
'@ark/schema@0.23.0':
|
||||||
resolution: {integrity: sha512-zpfXwWLOzj9aUK+dXQ6aleJAOgle4/WrHDop5CMX2M88dFQ85NdH8O0v0pvMAQnfFcaQAZ/nVDYLlBJsFc09XA==}
|
resolution: {integrity: sha512-406Zx0te3ICd7PkGise4XIxOfmjFzK64tEuiN5rmJDg14AqhySXygMk8QcHqHORDJ7VXhel7J41iduw8eyiFPg==}
|
||||||
|
|
||||||
'@ark/util@0.10.0':
|
'@ark/util@0.23.0':
|
||||||
resolution: {integrity: sha512-uK+9VU5doGMYOoOZVE+XaSs1vYACoaEJdrDkuBx26S4X7y3ChyKsPnIg/9pIw2vUySph1GkAXbvBnfVE2GmXgQ==}
|
resolution: {integrity: sha512-2mb24N2leQENRh+zPqnlRJzFFf8Xr7BT+/4MJN46/G8C45davpqFfcqvOw0ZlXrjQpBi8H+ZqDQsi95lN/9oVg==}
|
||||||
|
|
||||||
'@asteasolutions/zod-to-openapi@7.1.2':
|
'@asteasolutions/zod-to-openapi@7.1.2':
|
||||||
resolution: {integrity: sha512-tuDcV4aGAlY4eaZ8Qmf1efPL33hwJKdpCSbI6vJqXU5Wkz9IIyCrb3u3fExZyyMGzmLKcJH+CHI5UKvNBPlyjg==}
|
resolution: {integrity: sha512-tuDcV4aGAlY4eaZ8Qmf1efPL33hwJKdpCSbI6vJqXU5Wkz9IIyCrb3u3fExZyyMGzmLKcJH+CHI5UKvNBPlyjg==}
|
||||||
|
|
@ -1398,10 +1401,20 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
|
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
|
||||||
|
|
||||||
|
'@eslint-community/eslint-utils@4.4.1':
|
||||||
|
resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==}
|
||||||
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
|
||||||
|
|
||||||
'@eslint-community/regexpp@4.11.1':
|
'@eslint-community/regexpp@4.11.1':
|
||||||
resolution: {integrity: sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==}
|
resolution: {integrity: sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==}
|
||||||
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
||||||
|
|
||||||
|
'@eslint-community/regexpp@4.12.1':
|
||||||
|
resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
|
||||||
|
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
||||||
|
|
||||||
'@eslint/eslintrc@2.1.4':
|
'@eslint/eslintrc@2.1.4':
|
||||||
resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==}
|
resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
|
@ -2199,8 +2212,8 @@ packages:
|
||||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
'@playwright/test@1.48.2':
|
'@playwright/test@1.49.0':
|
||||||
resolution: {integrity: sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==}
|
resolution: {integrity: sha512-DMulbwQURa8rNIQrf94+jPJQ4FmOVdpE5ZppRNvWVjvhC+6sOeo28r8MgIpQRYouXRtt/FCCXU7zn20jnHR4Qw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
|
@ -2560,8 +2573,8 @@ packages:
|
||||||
resolution: {integrity: sha512-hg4ekaB5Y2zh+IWzBiC/WCDWrIfpVnKu/ubUvelKlidc/VbulsexoFRw5kJGHZenPVI5YzNnDeTdYSALkTV7jQ==}
|
resolution: {integrity: sha512-hg4ekaB5Y2zh+IWzBiC/WCDWrIfpVnKu/ubUvelKlidc/VbulsexoFRw5kJGHZenPVI5YzNnDeTdYSALkTV7jQ==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
'@vinejs/vine@1.8.0':
|
'@vinejs/vine@2.1.0':
|
||||||
resolution: {integrity: sha512-Qq3XxbA26jzqS9ICifkqzT399lMQZ2fWtqeV3luI2as+UIK7qDifJFU2Q4W3q3IB5VXoWxgwAZSZEO0em9I/qQ==}
|
resolution: {integrity: sha512-09aJ2OauxpblqiNqd8qC9RAzzm5SV6fTqZhE4e25j4cM7fmNoXRTjM7Oo8llFADMO4eSA44HqYEO3mkRRYdbYw==}
|
||||||
engines: {node: '>=18.16.0'}
|
engines: {node: '>=18.16.0'}
|
||||||
|
|
||||||
'@vitest/expect@1.6.0':
|
'@vitest/expect@1.6.0':
|
||||||
|
|
@ -2685,8 +2698,8 @@ packages:
|
||||||
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
|
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
arktype@2.0.0-rc.8:
|
arktype@2.0.0-rc.23:
|
||||||
resolution: {integrity: sha512-ByrqjptsavUCUL9ptts6BUL2LCNkVZyniOdaBw76dlBQ6gYIhYSeycuuj4gRFwcAafszOnAPD2fAqHK7bbo/Zw==}
|
resolution: {integrity: sha512-P0e40t3J4rc3xRHzPjzyOK1CgdgKswQJOFBgFLuehSiGcjAuRx6p/9lDVPzXZ62m7q5yRUqFiX8ovN5FjWQjMQ==}
|
||||||
|
|
||||||
array-flatten@1.1.1:
|
array-flatten@1.1.1:
|
||||||
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
|
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
|
||||||
|
|
@ -2788,8 +2801,8 @@ packages:
|
||||||
buffer@6.0.3:
|
buffer@6.0.3:
|
||||||
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
||||||
|
|
||||||
bullmq@5.25.6:
|
bullmq@5.27.0:
|
||||||
resolution: {integrity: sha512-jxpa/DB02V20CqBAgyqpQazT630CJm0r4fky8EchH3mcJAomRtKXLS6tRA0J8tb29BDGlr/LXhlUuZwdBJBSdA==}
|
resolution: {integrity: sha512-DZWrjDLkecZZ1/43h/SkG6CxU8nO/Lq/0svVoQdw33ksUCGfccgjbvCa/cuxHP/OvhxlTAA0cO3dBOoaT7sRFQ==}
|
||||||
|
|
||||||
bytes@3.1.2:
|
bytes@3.1.2:
|
||||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||||
|
|
@ -2966,6 +2979,10 @@ packages:
|
||||||
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
cross-spawn@7.0.6:
|
||||||
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
css-blank-pseudo@6.0.2:
|
css-blank-pseudo@6.0.2:
|
||||||
resolution: {integrity: sha512-J/6m+lsqpKPqWHOifAFtKFeGLOzw3jR92rxQcwRUfA/eTuZzKfKlxOmYDx2+tqOPQAueNvBiY8WhAeHu5qNmTg==}
|
resolution: {integrity: sha512-J/6m+lsqpKPqWHOifAFtKFeGLOzw3jR92rxQcwRUfA/eTuZzKfKlxOmYDx2+tqOPQAueNvBiY8WhAeHu5qNmTg==}
|
||||||
engines: {node: ^14 || ^16 || >=18}
|
engines: {node: ^14 || ^16 || >=18}
|
||||||
|
|
@ -3133,8 +3150,8 @@ packages:
|
||||||
resolution: {integrity: sha512-F6cFZ1wxa9XzFyeeQsp/0/lIzUbDuQjS8/njpYBDWa+wdWmXuY+Z/X2hHFK/9PGHZkv3c9mER+mVWfKlp/B6Vw==}
|
resolution: {integrity: sha512-F6cFZ1wxa9XzFyeeQsp/0/lIzUbDuQjS8/njpYBDWa+wdWmXuY+Z/X2hHFK/9PGHZkv3c9mER+mVWfKlp/B6Vw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
drizzle-orm@0.36.1:
|
drizzle-orm@0.36.3:
|
||||||
resolution: {integrity: sha512-F4hbimnMEhyWzDowQB4xEuVJJWXLHZYD7FYwvo8RImY+N7pStGqsbfmT95jDbec1s4qKmQbiuxEDZY90LRrfIw==}
|
resolution: {integrity: sha512-ffQB7CcyCTvQBK6xtRLMl/Jsd5xFTBs+UTHrgs1hbk68i5TPkbsoCPbKEwiEsQZfq2I7VH632XJpV1g7LS2H9Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@aws-sdk/client-rds-data': '>=3'
|
'@aws-sdk/client-rds-data': '>=3'
|
||||||
'@cloudflare/workers-types': '>=3'
|
'@cloudflare/workers-types': '>=3'
|
||||||
|
|
@ -3155,7 +3172,7 @@ packages:
|
||||||
'@xata.io/client': '*'
|
'@xata.io/client': '*'
|
||||||
better-sqlite3: '>=7'
|
better-sqlite3: '>=7'
|
||||||
bun-types: '*'
|
bun-types: '*'
|
||||||
expo-sqlite: '>=13.2.0'
|
expo-sqlite: '>=14.0.0'
|
||||||
knex: '*'
|
knex: '*'
|
||||||
kysely: '*'
|
kysely: '*'
|
||||||
mysql2: '>=2'
|
mysql2: '>=2'
|
||||||
|
|
@ -3465,8 +3482,8 @@ packages:
|
||||||
resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==}
|
resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==}
|
||||||
engines: {node: ^10.12.0 || >=12.0.0}
|
engines: {node: ^10.12.0 || >=12.0.0}
|
||||||
|
|
||||||
flatted@3.3.1:
|
flatted@3.3.2:
|
||||||
resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==}
|
resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==}
|
||||||
|
|
||||||
focus-trap@7.6.0:
|
focus-trap@7.6.0:
|
||||||
resolution: {integrity: sha512-1td0l3pMkWJLFipobUcGaf+5DTY4PLDDrcqoSaKP8ediO/CoWCCYk/fT/Y2A4e6TNB+Sh6clRJCjOPPnKoNHnQ==}
|
resolution: {integrity: sha512-1td0l3pMkWJLFipobUcGaf+5DTY4PLDDrcqoSaKP8ediO/CoWCCYk/fT/Y2A4e6TNB+Sh6clRJCjOPPnKoNHnQ==}
|
||||||
|
|
@ -4355,13 +4372,13 @@ packages:
|
||||||
pkg-types@1.2.0:
|
pkg-types@1.2.0:
|
||||||
resolution: {integrity: sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA==}
|
resolution: {integrity: sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA==}
|
||||||
|
|
||||||
playwright-core@1.48.2:
|
playwright-core@1.49.0:
|
||||||
resolution: {integrity: sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==}
|
resolution: {integrity: sha512-R+3KKTQF3npy5GTiKH/T+kdhoJfJojjHESR1YEWhYuEKRVfVaxH3+4+GvXE5xyCngCxhxnykk0Vlah9v8fs3jA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
playwright@1.48.2:
|
playwright@1.49.0:
|
||||||
resolution: {integrity: sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==}
|
resolution: {integrity: sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
|
@ -5129,8 +5146,8 @@ packages:
|
||||||
'@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
|
||||||
|
|
||||||
sveltekit-superforms@2.20.0:
|
sveltekit-superforms@2.20.1:
|
||||||
resolution: {integrity: sha512-5HyA6THKFBHEmJinZ/klu2/0jYr9ElSaXMYc5EO9ptP3x1wQPWVXYl59sMcaSrIjWUlPpayGxVppCyu+x/o4WA==}
|
resolution: {integrity: sha512-GPGPp4pf/v7fZ0iS3ddC1AO3Ti10oAgsVD95WZ6nPh6/L894pMsL8JN67/Lz0hSIRUXk8k35BjCIJB69z+KI/Q==}
|
||||||
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
|
||||||
|
|
@ -5152,8 +5169,8 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
tailwindcss: '>=3.0.0 || insiders'
|
tailwindcss: '>=3.0.0 || insiders'
|
||||||
|
|
||||||
tailwindcss@3.4.14:
|
tailwindcss@3.4.15:
|
||||||
resolution: {integrity: sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==}
|
resolution: {integrity: sha512-r4MeXnfBmSOuKUWmXe6h2CcyfzJCEk4F0pptO5jlnYSIViUkVmsawj80N5h2lO3gwcmSb4n3PuN+e+GC1Guylw==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
|
@ -5353,8 +5370,8 @@ packages:
|
||||||
valibot@0.31.1:
|
valibot@0.31.1:
|
||||||
resolution: {integrity: sha512-2YYIhPrnVSz/gfT2/iXVTrSj92HwchCt9Cga/6hX4B26iCz9zkIsGTS0HjDYTZfTi1Un0X6aRvhBi1cfqs/i0Q==}
|
resolution: {integrity: sha512-2YYIhPrnVSz/gfT2/iXVTrSj92HwchCt9Cga/6hX4B26iCz9zkIsGTS0HjDYTZfTi1Un0X6aRvhBi1cfqs/i0Q==}
|
||||||
|
|
||||||
valibot@0.41.0:
|
valibot@1.0.0-beta.6:
|
||||||
resolution: {integrity: sha512-igDBb8CTYr8YTQlOKgaN9nSS0Be7z+WRuaeYqGf3Cjz3aKmSnqEmYnkfVjzIuumGqfHpa3fLIvMEAfhrpqN8ng==}
|
resolution: {integrity: sha512-x9ObzhqDCWFaWOa6Zri1mbFcc8OIIKP7cQtD9JauKt5pJFhpJkvAXT+49bFKjoVikiKVk7m33mXgUJb/Wfknmw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=5'
|
typescript: '>=5'
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
|
|
@ -5556,12 +5573,12 @@ snapshots:
|
||||||
'@jridgewell/gen-mapping': 0.3.5
|
'@jridgewell/gen-mapping': 0.3.5
|
||||||
'@jridgewell/trace-mapping': 0.3.25
|
'@jridgewell/trace-mapping': 0.3.25
|
||||||
|
|
||||||
'@ark/schema@0.10.0':
|
'@ark/schema@0.23.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ark/util': 0.10.0
|
'@ark/util': 0.23.0
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@ark/util@0.10.0':
|
'@ark/util@0.23.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@asteasolutions/zod-to-openapi@7.1.2(zod@3.23.8)':
|
'@asteasolutions/zod-to-openapi@7.1.2(zod@3.23.8)':
|
||||||
|
|
@ -6248,8 +6265,15 @@ snapshots:
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-visitor-keys: 3.4.3
|
eslint-visitor-keys: 3.4.3
|
||||||
|
|
||||||
|
'@eslint-community/eslint-utils@4.4.1(eslint@8.57.1)':
|
||||||
|
dependencies:
|
||||||
|
eslint: 8.57.1
|
||||||
|
eslint-visitor-keys: 3.4.3
|
||||||
|
|
||||||
'@eslint-community/regexpp@4.11.1': {}
|
'@eslint-community/regexpp@4.11.1': {}
|
||||||
|
|
||||||
|
'@eslint-community/regexpp@4.12.1': {}
|
||||||
|
|
||||||
'@eslint/eslintrc@2.1.4':
|
'@eslint/eslintrc@2.1.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv: 6.12.6
|
ajv: 6.12.6
|
||||||
|
|
@ -6604,9 +6628,9 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.2.2
|
typescript: 5.2.2
|
||||||
|
|
||||||
'@lucia-auth/adapter-drizzle@1.1.0(drizzle-orm@0.36.1(@neondatabase/serverless@0.9.5)(@types/pg@8.11.10)(pg@8.13.1)(postgres@3.4.5))(lucia@3.2.0)':
|
'@lucia-auth/adapter-drizzle@1.1.0(drizzle-orm@0.36.3(@neondatabase/serverless@0.9.5)(@types/pg@8.11.10)(pg@8.13.1)(postgres@3.4.5))(lucia@3.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
drizzle-orm: 0.36.1(@neondatabase/serverless@0.9.5)(@types/pg@8.11.10)(pg@8.13.1)(postgres@3.4.5)
|
drizzle-orm: 0.36.3(@neondatabase/serverless@0.9.5)(@types/pg@8.11.10)(pg@8.13.1)(postgres@3.4.5)
|
||||||
lucia: 3.2.0
|
lucia: 3.2.0
|
||||||
|
|
||||||
'@lukeed/csprng@1.1.0': {}
|
'@lukeed/csprng@1.1.0': {}
|
||||||
|
|
@ -7104,9 +7128,9 @@ snapshots:
|
||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@playwright/test@1.48.2':
|
'@playwright/test@1.49.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
playwright: 1.48.2
|
playwright: 1.49.0
|
||||||
|
|
||||||
'@polka/url@1.0.0-next.28': {}
|
'@polka/url@1.0.0-next.28': {}
|
||||||
|
|
||||||
|
|
@ -7493,7 +7517,7 @@ snapshots:
|
||||||
'@vinejs/compiler@2.5.0':
|
'@vinejs/compiler@2.5.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@vinejs/vine@1.8.0':
|
'@vinejs/vine@2.1.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@poppinss/macroable': 1.0.3
|
'@poppinss/macroable': 1.0.3
|
||||||
'@types/validator': 13.12.2
|
'@types/validator': 13.12.2
|
||||||
|
|
@ -7551,9 +7575,9 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.12.1
|
acorn: 8.12.1
|
||||||
|
|
||||||
acorn-jsx@5.3.2(acorn@8.12.1):
|
acorn-jsx@5.3.2(acorn@8.14.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.12.1
|
acorn: 8.14.0
|
||||||
|
|
||||||
acorn-typescript@1.4.13(acorn@8.12.1):
|
acorn-typescript@1.4.13(acorn@8.12.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -7623,10 +7647,10 @@ snapshots:
|
||||||
|
|
||||||
aria-query@5.3.2: {}
|
aria-query@5.3.2: {}
|
||||||
|
|
||||||
arktype@2.0.0-rc.8:
|
arktype@2.0.0-rc.23:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ark/schema': 0.10.0
|
'@ark/schema': 0.23.0
|
||||||
'@ark/util': 0.10.0
|
'@ark/util': 0.23.0
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
array-flatten@1.1.1: {}
|
array-flatten@1.1.1: {}
|
||||||
|
|
@ -7742,7 +7766,7 @@ snapshots:
|
||||||
base64-js: 1.5.1
|
base64-js: 1.5.1
|
||||||
ieee754: 1.2.1
|
ieee754: 1.2.1
|
||||||
|
|
||||||
bullmq@5.25.6:
|
bullmq@5.27.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
cron-parser: 4.9.0
|
cron-parser: 4.9.0
|
||||||
ioredis: 5.4.1
|
ioredis: 5.4.1
|
||||||
|
|
@ -7915,6 +7939,12 @@ snapshots:
|
||||||
shebang-command: 2.0.0
|
shebang-command: 2.0.0
|
||||||
which: 2.0.2
|
which: 2.0.2
|
||||||
|
|
||||||
|
cross-spawn@7.0.6:
|
||||||
|
dependencies:
|
||||||
|
path-key: 3.1.1
|
||||||
|
shebang-command: 2.0.0
|
||||||
|
which: 2.0.2
|
||||||
|
|
||||||
css-blank-pseudo@6.0.2(postcss@8.4.49):
|
css-blank-pseudo@6.0.2(postcss@8.4.49):
|
||||||
dependencies:
|
dependencies:
|
||||||
postcss: 8.4.49
|
postcss: 8.4.49
|
||||||
|
|
@ -8032,16 +8062,16 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
drizzle-orm@0.36.1(@neondatabase/serverless@0.9.5)(@types/pg@8.11.10)(pg@8.13.1)(postgres@3.4.5):
|
drizzle-orm@0.36.3(@neondatabase/serverless@0.9.5)(@types/pg@8.11.10)(pg@8.13.1)(postgres@3.4.5):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@neondatabase/serverless': 0.9.5
|
'@neondatabase/serverless': 0.9.5
|
||||||
'@types/pg': 8.11.10
|
'@types/pg': 8.11.10
|
||||||
pg: 8.13.1
|
pg: 8.13.1
|
||||||
postgres: 3.4.5
|
postgres: 3.4.5
|
||||||
|
|
||||||
drizzle-zod@0.5.1(drizzle-orm@0.36.1(@neondatabase/serverless@0.9.5)(@types/pg@8.11.10)(pg@8.13.1)(postgres@3.4.5))(zod@3.23.8):
|
drizzle-zod@0.5.1(drizzle-orm@0.36.3(@neondatabase/serverless@0.9.5)(@types/pg@8.11.10)(pg@8.13.1)(postgres@3.4.5))(zod@3.23.8):
|
||||||
dependencies:
|
dependencies:
|
||||||
drizzle-orm: 0.36.1(@neondatabase/serverless@0.9.5)(@types/pg@8.11.10)(pg@8.13.1)(postgres@3.4.5)
|
drizzle-orm: 0.36.3(@neondatabase/serverless@0.9.5)(@types/pg@8.11.10)(pg@8.13.1)(postgres@3.4.5)
|
||||||
zod: 3.23.8
|
zod: 3.23.8
|
||||||
|
|
||||||
eastasianwidth@0.2.0: {}
|
eastasianwidth@0.2.0: {}
|
||||||
|
|
@ -8238,8 +8268,8 @@ snapshots:
|
||||||
|
|
||||||
eslint@8.57.1:
|
eslint@8.57.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.4.0(eslint@8.57.1)
|
'@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1)
|
||||||
'@eslint-community/regexpp': 4.11.1
|
'@eslint-community/regexpp': 4.12.1
|
||||||
'@eslint/eslintrc': 2.1.4
|
'@eslint/eslintrc': 2.1.4
|
||||||
'@eslint/js': 8.57.1
|
'@eslint/js': 8.57.1
|
||||||
'@humanwhocodes/config-array': 0.13.0
|
'@humanwhocodes/config-array': 0.13.0
|
||||||
|
|
@ -8248,7 +8278,7 @@ snapshots:
|
||||||
'@ungap/structured-clone': 1.2.0
|
'@ungap/structured-clone': 1.2.0
|
||||||
ajv: 6.12.6
|
ajv: 6.12.6
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
cross-spawn: 7.0.3
|
cross-spawn: 7.0.6
|
||||||
debug: 4.3.7
|
debug: 4.3.7
|
||||||
doctrine: 3.0.0
|
doctrine: 3.0.0
|
||||||
escape-string-regexp: 4.0.0
|
escape-string-regexp: 4.0.0
|
||||||
|
|
@ -8283,8 +8313,8 @@ snapshots:
|
||||||
|
|
||||||
espree@9.6.1:
|
espree@9.6.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.12.1
|
acorn: 8.14.0
|
||||||
acorn-jsx: 5.3.2(acorn@8.12.1)
|
acorn-jsx: 5.3.2(acorn@8.14.0)
|
||||||
eslint-visitor-keys: 3.4.3
|
eslint-visitor-keys: 3.4.3
|
||||||
|
|
||||||
esquery@1.6.0:
|
esquery@1.6.0:
|
||||||
|
|
@ -8444,11 +8474,11 @@ snapshots:
|
||||||
|
|
||||||
flat-cache@3.2.0:
|
flat-cache@3.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
flatted: 3.3.1
|
flatted: 3.3.2
|
||||||
keyv: 4.5.4
|
keyv: 4.5.4
|
||||||
rimraf: 3.0.2
|
rimraf: 3.0.2
|
||||||
|
|
||||||
flatted@3.3.1: {}
|
flatted@3.3.2: {}
|
||||||
|
|
||||||
focus-trap@7.6.0:
|
focus-trap@7.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -8467,11 +8497,11 @@ snapshots:
|
||||||
combined-stream: 1.0.8
|
combined-stream: 1.0.8
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
|
|
||||||
formsnap@1.0.1(svelte@5.0.0-next.175)(sveltekit-superforms@2.20.0(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(@types/json-schema@7.0.15)(svelte@5.0.0-next.175)(typescript@5.6.3)):
|
formsnap@1.0.1(svelte@5.0.0-next.175)(sveltekit-superforms@2.20.1(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(@types/json-schema@7.0.15)(svelte@5.0.0-next.175)(typescript@5.6.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
nanoid: 5.0.7
|
nanoid: 5.0.7
|
||||||
svelte: 5.0.0-next.175
|
svelte: 5.0.0-next.175
|
||||||
sveltekit-superforms: 2.20.0(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(@types/json-schema@7.0.15)(svelte@5.0.0-next.175)(typescript@5.6.3)
|
sveltekit-superforms: 2.20.1(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(@types/json-schema@7.0.15)(svelte@5.0.0-next.175)(typescript@5.6.3)
|
||||||
|
|
||||||
forwarded@0.2.0: {}
|
forwarded@0.2.0: {}
|
||||||
|
|
||||||
|
|
@ -9297,11 +9327,11 @@ snapshots:
|
||||||
mlly: 1.7.1
|
mlly: 1.7.1
|
||||||
pathe: 1.1.2
|
pathe: 1.1.2
|
||||||
|
|
||||||
playwright-core@1.48.2: {}
|
playwright-core@1.49.0: {}
|
||||||
|
|
||||||
playwright@1.48.2:
|
playwright@1.49.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
playwright-core: 1.48.2
|
playwright-core: 1.49.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.2
|
fsevents: 2.3.2
|
||||||
|
|
||||||
|
|
@ -10118,7 +10148,7 @@ snapshots:
|
||||||
'@sveltejs/kit': 2.8.1(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6))
|
'@sveltejs/kit': 2.8.1(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6))
|
||||||
svelte: 5.0.0-next.175
|
svelte: 5.0.0-next.175
|
||||||
|
|
||||||
sveltekit-superforms@2.20.0(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(@types/json-schema@7.0.15)(svelte@5.0.0-next.175)(typescript@5.6.3):
|
sveltekit-superforms@2.20.1(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(@types/json-schema@7.0.15)(svelte@5.0.0-next.175)(typescript@5.6.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sveltejs/kit': 2.8.1(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6))
|
'@sveltejs/kit': 2.8.1(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.6))
|
||||||
devalue: 5.1.1
|
devalue: 5.1.1
|
||||||
|
|
@ -10132,14 +10162,14 @@ snapshots:
|
||||||
'@gcornut/valibot-json-schema': 0.31.0
|
'@gcornut/valibot-json-schema': 0.31.0
|
||||||
'@sinclair/typebox': 0.32.35
|
'@sinclair/typebox': 0.32.35
|
||||||
'@typeschema/class-validator': 0.3.0(@types/json-schema@7.0.15)(class-validator@0.14.1)
|
'@typeschema/class-validator': 0.3.0(@types/json-schema@7.0.15)(class-validator@0.14.1)
|
||||||
'@vinejs/vine': 1.8.0
|
'@vinejs/vine': 2.1.0
|
||||||
arktype: 2.0.0-rc.8
|
arktype: 2.0.0-rc.23
|
||||||
class-validator: 0.14.1
|
class-validator: 0.14.1
|
||||||
effect: 3.9.2
|
effect: 3.9.2
|
||||||
joi: 17.13.3
|
joi: 17.13.3
|
||||||
json-schema-to-ts: 3.1.1
|
json-schema-to-ts: 3.1.1
|
||||||
superstruct: 2.0.2
|
superstruct: 2.0.2
|
||||||
valibot: 0.41.0(typescript@5.6.3)
|
valibot: 1.0.0-beta.6(typescript@5.6.3)
|
||||||
yup: 1.4.0
|
yup: 1.4.0
|
||||||
zod: 3.23.8
|
zod: 3.23.8
|
||||||
zod-to-json-schema: 3.23.5(zod@3.23.8)
|
zod-to-json-schema: 3.23.5(zod@3.23.8)
|
||||||
|
|
@ -10151,16 +10181,16 @@ snapshots:
|
||||||
|
|
||||||
tailwind-merge@2.5.4: {}
|
tailwind-merge@2.5.4: {}
|
||||||
|
|
||||||
tailwind-variants@0.2.1(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3))):
|
tailwind-variants@0.2.1(tailwindcss@3.4.15(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3))):
|
||||||
dependencies:
|
dependencies:
|
||||||
tailwind-merge: 2.5.4
|
tailwind-merge: 2.5.4
|
||||||
tailwindcss: 3.4.14(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3))
|
tailwindcss: 3.4.15(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3))
|
||||||
|
|
||||||
tailwindcss-animate@1.0.7(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3))):
|
tailwindcss-animate@1.0.7(tailwindcss@3.4.15(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3))):
|
||||||
dependencies:
|
dependencies:
|
||||||
tailwindcss: 3.4.14(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3))
|
tailwindcss: 3.4.15(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3))
|
||||||
|
|
||||||
tailwindcss@3.4.14(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)):
|
tailwindcss@3.4.15(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@alloc/quick-lru': 5.2.0
|
'@alloc/quick-lru': 5.2.0
|
||||||
arg: 5.0.2
|
arg: 5.0.2
|
||||||
|
|
@ -10175,7 +10205,7 @@ snapshots:
|
||||||
micromatch: 4.0.8
|
micromatch: 4.0.8
|
||||||
normalize-path: 3.0.0
|
normalize-path: 3.0.0
|
||||||
object-hash: 3.0.0
|
object-hash: 3.0.0
|
||||||
picocolors: 1.1.0
|
picocolors: 1.1.1
|
||||||
postcss: 8.4.49
|
postcss: 8.4.49
|
||||||
postcss-import: 15.1.0(postcss@8.4.49)
|
postcss-import: 15.1.0(postcss@8.4.49)
|
||||||
postcss-js: 4.0.1(postcss@8.4.49)
|
postcss-js: 4.0.1(postcss@8.4.49)
|
||||||
|
|
@ -10352,7 +10382,7 @@ snapshots:
|
||||||
valibot@0.31.1:
|
valibot@0.31.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
valibot@0.41.0(typescript@5.6.3):
|
valibot@1.0.0-beta.6(typescript@5.6.3):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.6.3
|
typescript: 5.6.3
|
||||||
optional: true
|
optional: true
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"user": {
|
|
||||||
"firstName": "John",
|
|
||||||
"lastName": "Doe",
|
|
||||||
"email": "johndoe@example.com",
|
|
||||||
"username": "johndoe"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"user": {
|
|
||||||
"firstName": "Jane",
|
|
||||||
"lastName": "Doe",
|
|
||||||
"email": "janedoe@example.com",
|
|
||||||
"username": "janedoe"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
@ -21,4 +21,7 @@ export const config: Config = {
|
||||||
migrating: env.DB_MIGRATING,
|
migrating: env.DB_MIGRATING,
|
||||||
seeding: env.DB_SEEDING,
|
seeding: env.DB_SEEDING,
|
||||||
},
|
},
|
||||||
|
security: {
|
||||||
|
encryptionKey: env.ENCRYPTION_KEY,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ const EnvSchema = z.object({
|
||||||
DB_MIGRATING: stringBoolean,
|
DB_MIGRATING: stringBoolean,
|
||||||
DB_SEEDING: stringBoolean,
|
DB_SEEDING: stringBoolean,
|
||||||
DOMAIN: z.string(),
|
DOMAIN: z.string(),
|
||||||
|
ENCRYPTION_KEY: z.string(),
|
||||||
GITHUB_CLIENT_ID: z.string(),
|
GITHUB_CLIENT_ID: z.string(),
|
||||||
GITHUB_CLIENT_SECRET: z.string(),
|
GITHUB_CLIENT_SECRET: z.string(),
|
||||||
GOOGLE_CLIENT_ID: z.string(),
|
GOOGLE_CLIENT_ID: z.string(),
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ export interface Config {
|
||||||
// storage: StorageConfig
|
// storage: StorageConfig
|
||||||
redis: RedisConfig;
|
redis: RedisConfig;
|
||||||
postgres: PostgresConfig;
|
postgres: PostgresConfig;
|
||||||
|
security: SecurityConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ApiConfig {
|
interface ApiConfig {
|
||||||
|
|
@ -33,3 +34,7 @@ interface PostgresConfig {
|
||||||
migrating: boolean;
|
migrating: boolean;
|
||||||
seeding: boolean;
|
seeding: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SecurityConfig {
|
||||||
|
encryptionKey: string;
|
||||||
|
}
|
||||||
|
|
@ -79,7 +79,7 @@ export class IamController extends Controller {
|
||||||
try {
|
try {
|
||||||
await this.iamService.updatePassword(user.id, { password, confirm_password });
|
await this.iamService.updatePassword(user.id, { password, confirm_password });
|
||||||
await this.sessionsService.invalidateSession(user.id);
|
await this.sessionsService.invalidateSession(user.id);
|
||||||
await this.loginRequestService.createUserSession(user.id, c.req, undefined);
|
await this.loginRequestService.createUserSession(user.id, c.req, false);
|
||||||
const sessionCookie = createBlankSessionTokenCookie();
|
const sessionCookie = createBlankSessionTokenCookie();
|
||||||
setSessionCookie(c, sessionCookie);
|
setSessionCookie(c, sessionCookie);
|
||||||
return c.json({ status: 'success' });
|
return c.json({ status: 'success' });
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,15 @@ import {zValidator} from '@hono/zod-validator';
|
||||||
import { inject, injectable } from '@needle-di/core';
|
import { inject, injectable } from '@needle-di/core';
|
||||||
import { CredentialsType } from '../databases/postgres/tables';
|
import { CredentialsType } from '../databases/postgres/tables';
|
||||||
import { requireAuth } from '../middleware/require-auth.middleware';
|
import { requireAuth } from '../middleware/require-auth.middleware';
|
||||||
|
import { createTwoFactorSchema } from '../dtos/create-totp.dto';
|
||||||
|
import { decodeBase64 } from '@oslojs/encoding';
|
||||||
|
import { LoginRequestsService } from '../services/loginrequest.service';
|
||||||
|
import { cookieExpiresAt, createSessionTokenCookie, setSessionCookie } from '../common/utils/cookies';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class MfaController extends Controller {
|
export class MfaController extends Controller {
|
||||||
constructor(
|
constructor(
|
||||||
|
private loginRequestService = inject(LoginRequestsService),
|
||||||
private recoveryCodesService = inject(RecoveryCodesService),
|
private recoveryCodesService = inject(RecoveryCodesService),
|
||||||
private totpService = inject(TotpService),
|
private totpService = inject(TotpService),
|
||||||
private usersService = inject(UsersService),
|
private usersService = inject(UsersService),
|
||||||
|
|
@ -26,10 +31,15 @@ export class MfaController extends Controller {
|
||||||
const totpCredential = await this.totpService.findOneByUserId(user.id);
|
const totpCredential = await this.totpService.findOneByUserId(user.id);
|
||||||
return c.json({ totpCredential });
|
return c.json({ totpCredential });
|
||||||
})
|
})
|
||||||
.post('/totp', requireAuth, async (c) => {
|
.post('/totp', requireAuth, zValidator('json', createTwoFactorSchema), async (c) => {
|
||||||
const user = c.var.user;
|
const user = c.var.user;
|
||||||
const totpCredential = await this.totpService.create(user.id);
|
const { key } = c.req.valid('json');
|
||||||
|
const totpCredential = await this.totpService.create(user.id, decodeBase64(key));
|
||||||
|
if (totpCredential) {
|
||||||
|
await this.usersService.updateUser(user.id, { mfa_enabled: true });
|
||||||
return c.json({ totpCredential });
|
return c.json({ totpCredential });
|
||||||
|
}
|
||||||
|
return c.status(StatusCodes.INTERNAL_SERVER_ERROR);
|
||||||
})
|
})
|
||||||
.delete('/totp', requireAuth, async (c) => {
|
.delete('/totp', requireAuth, async (c) => {
|
||||||
const user = c.var.user;
|
const user = c.var.user;
|
||||||
|
|
@ -50,18 +60,40 @@ export class MfaController extends Controller {
|
||||||
const existingCodes = await this.recoveryCodesService.findAllRecoveryCodesByUserId(user.id);
|
const existingCodes = await this.recoveryCodesService.findAllRecoveryCodesByUserId(user.id);
|
||||||
if (existingCodes && existingCodes.length > 0) {
|
if (existingCodes && existingCodes.length > 0) {
|
||||||
console.log('Recovery Codes found', existingCodes);
|
console.log('Recovery Codes found', existingCodes);
|
||||||
return c.json({ recoveryCodes: existingCodes });
|
// Filter out codes that are not used and only return the code
|
||||||
|
const codes = existingCodes.filter(code => !code.used).map(code => code.code);
|
||||||
|
return c.json({ recoveryCodes: codes });
|
||||||
}
|
}
|
||||||
const recoveryCodes = await this.recoveryCodesService.createRecoveryCodes(user.id);
|
const recoveryCodes = await this.recoveryCodesService.createRecoveryCodes(user.id);
|
||||||
return c.json({ recoveryCodes });
|
return c.json({ recoveryCodes });
|
||||||
})
|
})
|
||||||
|
.post('/totp/recoveryCodes', requireAuth, zValidator('json', verifyTotpDto), async (c) => {
|
||||||
|
try {
|
||||||
|
const user = c.var.user;
|
||||||
|
const { code } = c.req.valid('json');
|
||||||
|
c.var.logger.info(`Verifying code ${code} for user ${user.id}`);
|
||||||
|
const verified = await this.recoveryCodesService.verify(user.id, code);
|
||||||
|
if (verified) {
|
||||||
|
return c.json({}, StatusCodes.OK);
|
||||||
|
}
|
||||||
|
return c.json('Invalid code', StatusCodes.BAD_REQUEST);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return c.status(StatusCodes.INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
})
|
||||||
.post('/totp/verify', requireAuth, zValidator('json', verifyTotpDto), async (c) => {
|
.post('/totp/verify', requireAuth, zValidator('json', verifyTotpDto), async (c) => {
|
||||||
try {
|
try {
|
||||||
const user = c.var.user;
|
const user = c.var.user;
|
||||||
const { code } = c.req.valid('json');
|
const { code } = c.req.valid('json');
|
||||||
|
c.var.logger.info(`Verifying code ${code} for user ${user.id}`);
|
||||||
const verified = await this.totpService.verify(user.id, code);
|
const verified = await this.totpService.verify(user.id, code);
|
||||||
if (verified) {
|
if (verified) {
|
||||||
await this.usersService.updateUser(user.id, { mfa_enabled: true });
|
await this.usersService.updateUser(user.id, { mfa_enabled: true });
|
||||||
|
const session = await this.loginRequestService.createUserSession(user.id, c.req, true);
|
||||||
|
const sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt);
|
||||||
|
console.log('set cookie', sessionCookie);
|
||||||
|
setSessionCookie(c, sessionCookie);
|
||||||
return c.json({}, StatusCodes.OK);
|
return c.json({}, StatusCodes.OK);
|
||||||
}
|
}
|
||||||
return c.json('Invalid code', StatusCodes.BAD_REQUEST);
|
return c.json('Invalid code', StatusCodes.BAD_REQUEST);
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ export class SignupController extends Controller {
|
||||||
return c.body('Failed to create user', 500);
|
return c.body('Failed to create user', 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = await this.loginRequestService.createUserSession(user.id, c.req, undefined);
|
const session = await this.loginRequestService.createUserSession(user.id, c.req, false);
|
||||||
const sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt);
|
const sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt);
|
||||||
console.log('set cookie', sessionCookie);
|
console.log('set cookie', sessionCookie);
|
||||||
setSessionCookie(c, sessionCookie);
|
setSessionCookie(c, sessionCookie);
|
||||||
|
|
|
||||||
7
src/lib/server/api/dtos/create-totp.dto.ts
Normal file
7
src/lib/server/api/dtos/create-totp.dto.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const createTwoFactorSchema = z.object({
|
||||||
|
key: z.string({ required_error: 'Secret Data is required' }).length(28, { message: 'Secret Data must be 28 characters' }).trim(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CreateTwoFactorDto = z.infer<typeof createTwoFactorSchema>;
|
||||||
|
|
@ -1,11 +1,7 @@
|
||||||
import {z} from "zod";
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const signinUsernameDto = z.object({
|
export const signinUsernameDto = z.object({
|
||||||
username: z
|
username: z.string().trim().min(3, { message: 'Must be at least 3 characters' }).max(50, { message: 'Must be less than 50 characters' }),
|
||||||
.string()
|
|
||||||
.trim()
|
|
||||||
.min(3, { message: 'Must be at least 3 characters' })
|
|
||||||
.max(50, { message: 'Must be less than 50 characters' }),
|
|
||||||
password: z.string({ required_error: 'Password is required' }).trim(),
|
password: z.string({ required_error: 'Password is required' }).trim(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import {takeFirstOrThrow} from '$lib/server/api/common/utils/repository';
|
import {takeFirstOrThrow} from '$lib/server/api/common/utils/repository';
|
||||||
import {DrizzleService} from '$lib/server/api/services/drizzle.service';
|
import {DrizzleService} from '$lib/server/api/services/drizzle.service';
|
||||||
import {eq, type InferInsertModel} from 'drizzle-orm';
|
import {and, eq, type InferInsertModel} from 'drizzle-orm';
|
||||||
import {inject, injectable} from '@needle-di/core';
|
import {inject, injectable} from '@needle-di/core';
|
||||||
import {recoveryCodesTable} from '../databases/postgres/tables';
|
import {recoveryCodesTable} from '../databases/postgres/tables';
|
||||||
|
|
||||||
|
|
@ -20,6 +20,12 @@ export class RecoveryCodesRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findAllNotUsedByUserId(userId: string, db = this.drizzle.db) {
|
||||||
|
return db.query.recoveryCodesTable.findMany({
|
||||||
|
where: and(eq(recoveryCodesTable.userId, userId), eq(recoveryCodesTable.used, false)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async deleteAllByUserId(userId: string, db = this.drizzle.db) {
|
async deleteAllByUserId(userId: string, db = this.drizzle.db) {
|
||||||
return db.delete(recoveryCodesTable).where(eq(recoveryCodesTable.userId, userId));
|
return db.delete(recoveryCodesTable).where(eq(recoveryCodesTable.userId, userId));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
46
src/lib/server/api/services/encryption.service.ts
Normal file
46
src/lib/server/api/services/encryption.service.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { decodeBase64 } from '@oslojs/encoding';
|
||||||
|
import { createCipheriv, createDecipheriv } from 'crypto';
|
||||||
|
import { DynamicBuffer } from '@oslojs/binary';
|
||||||
|
import { injectable } from '@needle-di/core';
|
||||||
|
import { config } from '../common/config';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class EncryptionService {
|
||||||
|
private encryptionKey: Uint8Array;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.encryptionKey = decodeBase64(config.security.encryptionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypt(data: Uint8Array): Uint8Array {
|
||||||
|
const iv = new Uint8Array(16);
|
||||||
|
crypto.getRandomValues(iv);
|
||||||
|
const cipher = createCipheriv('aes-128-gcm', this.encryptionKey, iv);
|
||||||
|
const encrypted = new DynamicBuffer(0);
|
||||||
|
encrypted.write(iv);
|
||||||
|
encrypted.write(cipher.update(data));
|
||||||
|
encrypted.write(cipher.final());
|
||||||
|
encrypted.write(cipher.getAuthTag());
|
||||||
|
return encrypted.bytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptString(data: string): Uint8Array {
|
||||||
|
return this.encrypt(new TextEncoder().encode(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
decrypt(encrypted: Uint8Array): Uint8Array {
|
||||||
|
if (encrypted.byteLength < 33) {
|
||||||
|
throw new Error('Invalid data');
|
||||||
|
}
|
||||||
|
const decipher = createDecipheriv('aes-128-gcm', this.encryptionKey, encrypted.slice(0, 16));
|
||||||
|
decipher.setAuthTag(encrypted.slice(encrypted.byteLength - 16));
|
||||||
|
const decrypted = new DynamicBuffer(0);
|
||||||
|
decrypted.write(decipher.update(encrypted.slice(16, encrypted.byteLength - 16)));
|
||||||
|
decrypted.write(decipher.final());
|
||||||
|
return decrypted.bytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
decryptToString(data: Uint8Array): string {
|
||||||
|
return new TextDecoder().decode(this.decrypt(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,7 @@ import {CredentialsRepository} from '../repositories/credentials.repository';
|
||||||
import { UsersRepository } from '../repositories/users.repository';
|
import { UsersRepository } from '../repositories/users.repository';
|
||||||
import { MailerService } from './mailer.service';
|
import { MailerService } from './mailer.service';
|
||||||
import { TokensService } from './tokens.service';
|
import { TokensService } from './tokens.service';
|
||||||
import {DrizzleService} from "$lib/server/api/services/drizzle.service";
|
import { DrizzleService } from '$lib/server/api/services/drizzle.service';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class LoginRequestsService {
|
export class LoginRequestsService {
|
||||||
|
|
@ -34,7 +34,7 @@ export class LoginRequestsService {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
async verify(data: SigninUsernameDto, req: HonoRequest) {
|
async verify(data: SigninUsernameDto, req: HonoRequest) {
|
||||||
const requestIpAddress = req.header('x-real-ip');
|
const requestIpAddress = req.header('X-Forwarded-For');
|
||||||
const requestIpCountry = req.header('x-vercel-ip-country');
|
const requestIpCountry = req.header('x-vercel-ip-country');
|
||||||
const existingUser = await this.usersRepository.findOneByUsername(data.username);
|
const existingUser = await this.usersRepository.findOneByUsername(data.username);
|
||||||
|
|
||||||
|
|
@ -54,18 +54,18 @@ export class LoginRequestsService {
|
||||||
|
|
||||||
const totpCredentials = await this.credentialsRepository.findTOTPCredentialsByUserId(existingUser.id);
|
const totpCredentials = await this.credentialsRepository.findTOTPCredentialsByUserId(existingUser.id);
|
||||||
|
|
||||||
return await this.createUserSession(existingUser.id, req, totpCredentials);
|
return await this.createUserSession(existingUser.id, req, !!totpCredentials && totpCredentials.secret_data !== null && totpCredentials.secret_data !== '');
|
||||||
}
|
}
|
||||||
|
|
||||||
async createUserSession(existingUserId: string, req: HonoRequest, totpCredentials: Credentials | undefined) {
|
async createUserSession(existingUserId: string, req: HonoRequest, twoFactorAuthEnabled: boolean) {
|
||||||
const requestIpAddress = req.header('x-real-ip');
|
const requestIpAddress = req.header('X-Forwarded-For');
|
||||||
const requestIpCountry = req.header('x-vercel-ip-country');
|
const requestIpCountry = req.header('x-vercel-ip-country');
|
||||||
return this.sessionsService.createSession(
|
return this.sessionsService.createSession(
|
||||||
this.sessionsService.generateSessionToken(),
|
this.sessionsService.generateSessionToken(),
|
||||||
existingUserId,
|
existingUserId,
|
||||||
requestIpCountry || 'unknown',
|
requestIpCountry || 'unknown',
|
||||||
requestIpAddress || 'unknown',
|
requestIpAddress || 'unknown',
|
||||||
!!totpCredentials && totpCredentials?.secret_data !== null && totpCredentials?.secret_data !== '',
|
twoFactorAuthEnabled,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,11 @@ export class RecoveryCodesService {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async verify(userId: string, code: string) {
|
||||||
|
const recoveryCodes = await this.recoveryCodesRepository.findAllNotUsedByUserId(userId);
|
||||||
|
return recoveryCodes.find(recoveryCode => this.hashingService.verify(recoveryCode.code, code))
|
||||||
|
}
|
||||||
|
|
||||||
async deleteAllRecoveryCodesByUserId(userId: string) {
|
async deleteAllRecoveryCodesByUserId(userId: string) {
|
||||||
return this.recoveryCodesRepository.deleteAllByUserId(userId)
|
return this.recoveryCodesRepository.deleteAllByUserId(userId)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
import { CredentialsRepository } from '$lib/server/api/repositories/credentials.repository';
|
import { CredentialsRepository } from '$lib/server/api/repositories/credentials.repository';
|
||||||
import {decodeHex, encodeHexLowerCase} from '@oslojs/encoding';
|
|
||||||
import {verifyTOTP} from '@oslojs/otp';
|
|
||||||
import { inject, injectable } from '@needle-di/core';
|
import { inject, injectable } from '@needle-di/core';
|
||||||
|
import { decodeBase64, encodeBase64 } from "@oslojs/encoding";
|
||||||
|
import { generateTOTP, verifyTOTP } from '@oslojs/otp';
|
||||||
import type { CredentialsType } from '../databases/postgres/tables';
|
import type { CredentialsType } from '../databases/postgres/tables';
|
||||||
|
import { EncryptionService } from './encryption.service';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class TotpService {
|
export class TotpService {
|
||||||
constructor(private credentialsRepository = inject(CredentialsRepository)) {}
|
constructor(
|
||||||
|
private credentialsRepository = inject(CredentialsRepository),
|
||||||
|
private encryptionService = inject(EncryptionService)
|
||||||
|
) {}
|
||||||
|
|
||||||
async findOneByUserId(userId: string) {
|
async findOneByUserId(userId: string) {
|
||||||
return this.credentialsRepository.findTOTPCredentialsByUserId(userId);
|
return this.credentialsRepository.findTOTPCredentialsByUserId(userId);
|
||||||
|
|
@ -20,12 +24,11 @@ export class TotpService {
|
||||||
return credential;
|
return credential;
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(userId: string) {
|
async create(userId: string, key: Uint8Array) {
|
||||||
const secret = new Uint8Array(20);
|
|
||||||
try {
|
try {
|
||||||
return await this.credentialsRepository.create({
|
return await this.credentialsRepository.create({
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
secret_data: encodeHexLowerCase(crypto.getRandomValues(secret)),
|
secret_data: encodeBase64(this.encryptionService.encrypt(key)),
|
||||||
type: 'totp',
|
type: 'totp',
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -44,9 +47,11 @@ export class TotpService {
|
||||||
|
|
||||||
async verify(userId: string, code: string) {
|
async verify(userId: string, code: string) {
|
||||||
const credential = await this.credentialsRepository.findTOTPCredentialsByUserId(userId);
|
const credential = await this.credentialsRepository.findTOTPCredentialsByUserId(userId);
|
||||||
|
console.log(`TOTP credential: ${JSON.stringify(credential)}`);
|
||||||
if (!credential) {
|
if (!credential) {
|
||||||
throw new Error('TOTP credential not found');
|
throw new Error('TOTP credential not found');
|
||||||
}
|
}
|
||||||
return verifyTOTP(decodeHex(credential.secret_data), 30, 6, code);
|
const secret = this.encryptionService.decrypt(decodeBase64(credential.secret_data));
|
||||||
|
return verifyTOTP(secret, 30, 6, code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
|
import { z } from 'zod';
|
||||||
import { refinePasswords } from './account';
|
import { refinePasswords } from './account';
|
||||||
import { userSchema } from './zod-schemas';
|
import { userSchema } from './zod-schemas';
|
||||||
import {z} from 'zod';
|
|
||||||
|
|
||||||
export const signUpSchema = userSchema
|
export const signUpSchema = userSchema
|
||||||
.pick({
|
.pick({
|
||||||
|
|
@ -16,16 +16,12 @@ export const signUpSchema = userSchema
|
||||||
});
|
});
|
||||||
|
|
||||||
export const signInSchema = z.object({
|
export const signInSchema = z.object({
|
||||||
username: z
|
username: z.string().trim().min(3, { message: 'Must be at least 3 characters' }).max(50, { message: 'Must be less than 50 characters' }),
|
||||||
.string()
|
|
||||||
.trim()
|
|
||||||
.min(3, { message: 'Must be at least 3 characters' })
|
|
||||||
.max(50, { message: 'Must be less than 50 characters' }),
|
|
||||||
password: z.string({ required_error: 'Password is required' }).trim(),
|
password: z.string({ required_error: 'Password is required' }).trim(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const totpSchema = z.object({
|
export const totpSchema = z.object({
|
||||||
totpToken: z.string().trim().min(6).max(6),
|
code: z.string().trim().min(6).max(6),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const recoveryCodeSchema = z.object({
|
export const recoveryCodeSchema = z.object({
|
||||||
|
|
|
||||||
|
|
@ -24,5 +24,5 @@ export const load: PageServerLoad = async (event) => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
redirect(302, '/profile', { message: 'Two-Factor Authentication is not enabled', type: 'error' }, event);
|
redirect(302, '/settings/profile', { message: 'Two-Factor Authentication is not enabled', type: 'error' }, event);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { notSignedInMessage } from '$lib/flashMessages';
|
import { notSignedInMessage } from '$lib/flashMessages';
|
||||||
import env from '$lib/server/api/common/env';
|
import env from '$lib/server/api/common/env';
|
||||||
import { decodeHex, encodeBase32 } from '@oslojs/encoding';
|
import { decodeBase64, encodeBase32NoPadding, encodeBase64 } from '@oslojs/encoding';
|
||||||
import { createTOTPKeyURI } from '@oslojs/otp';
|
import { createTOTPKeyURI, verifyTOTP } from '@oslojs/otp';
|
||||||
import { type Actions, fail } from '@sveltejs/kit';
|
import { type Actions, fail } from '@sveltejs/kit';
|
||||||
import kebabCase from 'just-kebab-case';
|
import kebabCase from 'just-kebab-case';
|
||||||
import QRCode from 'qrcode';
|
import QRCode from 'qrcode';
|
||||||
|
|
@ -21,66 +21,44 @@ export const load: PageServerLoad = async (event) => {
|
||||||
|
|
||||||
const addTwoFactorForm = await superValidate(event, zod(addTwoFactorSchema));
|
const addTwoFactorForm = await superValidate(event, zod(addTwoFactorSchema));
|
||||||
const removeTwoFactorForm = await superValidate(event, zod(removeTwoFactorSchema));
|
const removeTwoFactorForm = await superValidate(event, zod(removeTwoFactorSchema));
|
||||||
// const addAuthNFactorForm = await superValidate(event, zod(addAuthNFactorSchema));
|
|
||||||
|
|
||||||
const { data, error } = await locals.api.mfa.totp.$get().then(locals.parseApiResponse);
|
const { data: twoFactorCredentials, error: twoFactorCredentialsError } = await locals.api.mfa.totp.$get().then(locals.parseApiResponse);
|
||||||
if (error || !data) {
|
if (twoFactorCredentials?.totpCredential) {
|
||||||
return fail(500, {
|
|
||||||
addTwoFactorForm,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const { totpCredential } = data;
|
|
||||||
if (totpCredential && authedUser.mfa_enabled) {
|
|
||||||
return {
|
return {
|
||||||
addTwoFactorForm,
|
addTwoFactorForm,
|
||||||
removeTwoFactorForm,
|
removeTwoFactorForm,
|
||||||
twoFactorEnabled: true,
|
twoFactorEnabled: true,
|
||||||
recoveryCodes: [],
|
recoveryCodes: [],
|
||||||
totpUri: '',
|
keyURI: '',
|
||||||
|
secret: '',
|
||||||
qrCode: '',
|
qrCode: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totpCredential && !authedUser.mfa_enabled) {
|
|
||||||
await locals.api.mfa.totp.$delete().then(locals.parseApiResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
const issuer = kebabCase(env.PUBLIC_SITE_NAME);
|
const issuer = kebabCase(env.PUBLIC_SITE_NAME);
|
||||||
const accountName = authedUser.email || authedUser.username;
|
const accountName = authedUser.email || authedUser.username;
|
||||||
const { data: createdTotpData, error: createdTotpError } = await locals.api.mfa.totp.$post().then(locals.parseApiResponse);
|
const totpKey = new Uint8Array(20);
|
||||||
|
crypto.getRandomValues(totpKey);
|
||||||
if (createdTotpError || !createdTotpData) {
|
const encodedTOTPKey = encodeBase64(totpKey);
|
||||||
return fail(500, {
|
|
||||||
addTwoFactorForm,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { totpCredential: createdTotpCredentials } = createdTotpData;
|
|
||||||
// pass the website's name and the user identifier (e.g. email, username)
|
|
||||||
if (!createdTotpCredentials?.secret_data) {
|
|
||||||
return fail(500, {
|
|
||||||
addTwoFactorForm,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const decodedHexSecret = decodeHex(createdTotpCredentials.secret_data);
|
|
||||||
const secret = encodeBase32(decodedHexSecret);
|
|
||||||
const intervalInSeconds = 30;
|
const intervalInSeconds = 30;
|
||||||
const digits = 6;
|
const digits = 6;
|
||||||
|
|
||||||
const totpUri = createTOTPKeyURI(issuer, accountName, decodedHexSecret, intervalInSeconds, digits);
|
const keyURI = createTOTPKeyURI(issuer, accountName, totpKey, intervalInSeconds, digits);
|
||||||
|
console.log('keyURI', keyURI);
|
||||||
|
|
||||||
addTwoFactorForm.data = {
|
addTwoFactorForm.data = {
|
||||||
password: '',
|
password: '',
|
||||||
two_factor_code: '',
|
code: '',
|
||||||
|
key: encodedTOTPKey,
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
addTwoFactorForm,
|
addTwoFactorForm,
|
||||||
removeTwoFactorForm,
|
removeTwoFactorForm,
|
||||||
twoFactorEnabled: false,
|
twoFactorEnabled: false,
|
||||||
recoveryCodes: [],
|
recoveryCodes: [],
|
||||||
totpUri,
|
keyURI,
|
||||||
qrCode: await QRCode.toDataURL(totpUri),
|
secret: encodeBase32NoPadding(totpKey),
|
||||||
secret,
|
qrCode: await QRCode.toDataURL(keyURI),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -112,18 +90,37 @@ export const actions: Actions = {
|
||||||
return setError(addTwoFactorForm, 'password', 'Your password is incorrect');
|
return setError(addTwoFactorForm, 'password', 'Your password is incorrect');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (addTwoFactorForm.data.two_factor_code === '') {
|
if (addTwoFactorForm.data.code === '') {
|
||||||
return setError(addTwoFactorForm, 'two_factor_code', 'Please enter a code');
|
return setError(addTwoFactorForm, 'code', 'Please enter a code');
|
||||||
}
|
}
|
||||||
|
|
||||||
const twoFactorCode = addTwoFactorForm.data.two_factor_code;
|
const twoFactorCode = addTwoFactorForm.data.code;
|
||||||
const { error: verifyTotpError } = await locals.api.mfa.totp.verify
|
const encodedKey = addTwoFactorForm.data.key;
|
||||||
|
|
||||||
|
let key: Uint8Array;
|
||||||
|
try {
|
||||||
|
key = decodeBase64(encodedKey);
|
||||||
|
} catch {
|
||||||
|
return fail(400, {
|
||||||
|
message: 'Invalid key',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (key.byteLength !== 20) {
|
||||||
|
return fail(400, {
|
||||||
|
message: 'Invalid key',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!verifyTOTP(key, 30, 6, twoFactorCode)) {
|
||||||
|
return setError(addTwoFactorForm, 'code', 'Invalid code');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error: createTotpError } = await locals.api.mfa.totp
|
||||||
.$post({
|
.$post({
|
||||||
json: { code: twoFactorCode },
|
json: { key: encodeBase64(key) },
|
||||||
})
|
})
|
||||||
.then(locals.parseApiResponse);
|
.then(locals.parseApiResponse);
|
||||||
if (verifyTotpError) {
|
if (createTotpError) {
|
||||||
return setError(addTwoFactorForm, 'two_factor_code', 'Invalid code');
|
return setError(addTwoFactorForm, 'code', 'Invalid code');
|
||||||
}
|
}
|
||||||
|
|
||||||
redirect(302, '/settings/security/mfa/recovery-codes');
|
redirect(302, '/settings/security/mfa/recovery-codes');
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { addTwoFactorSchema, removeTwoFactorSchema } from './schemas';
|
||||||
|
|
||||||
const { data } = $props();
|
const { data } = $props();
|
||||||
|
|
||||||
const { qrCode, secret, twoFactorEnabled, recoveryCodes } = data;
|
const { qrCode, twoFactorEnabled, recoveryCodes, secret } = data;
|
||||||
|
|
||||||
const addTwoFactorForm = superForm(data.addTwoFactorForm, {
|
const addTwoFactorForm = superForm(data.addTwoFactorForm, {
|
||||||
taintedMessage: null,
|
taintedMessage: null,
|
||||||
|
|
@ -52,10 +52,10 @@ const { form: removeTwoFactorFormData, enhance: removeTwoFactorEnhance } = remov
|
||||||
<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" action="?/enableTotp" use:addTwoFactorEnhance data-sveltekit-replacestate>
|
<form method="POST" action="?/enableTotp" use:addTwoFactorEnhance data-sveltekit-replacestate>
|
||||||
<Form.Field form={addTwoFactorForm} name="two_factor_code">
|
<Form.Field form={addTwoFactorForm} name="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>
|
||||||
<PinInput {...attrs} bind:value={$addTwoFactorFormData.two_factor_code} />
|
<PinInput {...attrs} bind:value={$addTwoFactorFormData.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 />
|
||||||
|
|
@ -68,6 +68,7 @@ const { form: removeTwoFactorFormData, enhance: removeTwoFactorEnhance } = remov
|
||||||
<Form.Description>Please enter your current password.</Form.Description>
|
<Form.Description>Please enter your current password.</Form.Description>
|
||||||
<Form.FieldErrors />
|
<Form.FieldErrors />
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
|
<input name="key" type="hidden" value={$addTwoFactorFormData.key} hidden required />
|
||||||
<Form.Button>Submit</Form.Button>
|
<Form.Button>Submit</Form.Button>
|
||||||
</form>
|
</form>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@ import { z } from 'zod';
|
||||||
|
|
||||||
export const addTwoFactorSchema = z.object({
|
export const addTwoFactorSchema = z.object({
|
||||||
password: z.string({ required_error: 'Current Password is required' }),
|
password: z.string({ required_error: 'Current Password is required' }),
|
||||||
two_factor_code: z.string({ required_error: 'Two Factor Code is required' }).trim(),
|
code: z.string({ required_error: 'Two Factor Code is required' }).trim(),
|
||||||
|
key: z.string({ required_error: 'Secret Data is required' }).length(28).trim(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AddTwoFactorSchema = typeof addTwoFactorSchema;
|
export type AddTwoFactorSchema = typeof addTwoFactorSchema;
|
||||||
|
|
|
||||||
|
|
@ -52,19 +52,24 @@ export const actions: Actions = {
|
||||||
form.data.username = '';
|
form.data.username = '';
|
||||||
form.data.password = '';
|
form.data.password = '';
|
||||||
|
|
||||||
redirect(StatusCodes.TEMPORARY_REDIRECT, '/');
|
const { error: totpCredentialError, data } = await locals.api.mfa.totp.$get().then(locals.parseApiResponse);
|
||||||
|
if (totpCredentialError || !data) {
|
||||||
|
return setError(form, 'username', totpCredentialError ?? 'Something went wrong. Please try again.');
|
||||||
|
}
|
||||||
|
|
||||||
// if (
|
const { totpCredential } = data;
|
||||||
// twoFactorDetails?.enabled &&
|
console.log('totpCredential', totpCredential);
|
||||||
// twoFactorDetails?.secret !== null &&
|
if (!totpCredential) {
|
||||||
// twoFactorDetails?.secret !== ''
|
const message = { type: 'success', message: 'Signed In!' } as const;
|
||||||
// ) {
|
redirect(302, '/', message, event);
|
||||||
// console.log('redirecting to TOTP page');
|
} else if (totpCredential?.type === 'totp' && totpCredential?.secret_data && totpCredential?.secret_data !== '') {
|
||||||
// const message = { type: 'success', message: 'Please enter your TOTP code.' } as const;
|
console.log('redirecting to TOTP page');
|
||||||
// redirect(302, '/totp', message, event);
|
const message = { type: 'success', message: 'Please enter your TOTP code.' } as const;
|
||||||
// } else {
|
redirect(302, '/totp', message, event);
|
||||||
// const message = { type: 'success', message: 'Signed In!' } as const;
|
} else {
|
||||||
// redirect(302, '/', message, event);
|
return setError(form, 'username', 'Something went wrong. Please try again.');
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
redirect(StatusCodes.TEMPORARY_REDIRECT, '/');
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import { notSignedInMessage } from '$lib/flashMessages';
|
import { notSignedInMessage } from '$lib/flashMessages';
|
||||||
import env from '$lib/server/api/common/env';
|
import env from '$lib/server/api/common/env';
|
||||||
|
import { twoFactorTable, usersTable } from '$lib/server/api/databases/postgres/tables';
|
||||||
import { db } from '$lib/server/api/packages/drizzle';
|
import { db } from '$lib/server/api/packages/drizzle';
|
||||||
import { recoveryCodeSchema, totpSchema } from '$lib/validations/auth';
|
import { recoveryCodeSchema, totpSchema } from '$lib/validations/auth';
|
||||||
|
import { updateProfileFormSchema } from '$routes/(app)/(protected)/settings/profile/schemas';
|
||||||
import { type Actions, fail } from '@sveltejs/kit';
|
import { type Actions, fail } from '@sveltejs/kit';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { redirect } from 'sveltekit-flash-message/server';
|
import { redirect } from 'sveltekit-flash-message/server';
|
||||||
import { zod } from 'sveltekit-superforms/adapters';
|
import { zod } from 'sveltekit-superforms/adapters';
|
||||||
import { superValidate } from 'sveltekit-superforms/server';
|
import { message, setError, superValidate } from 'sveltekit-superforms/server';
|
||||||
import { twoFactorTable, usersTable } from '../../../lib/server/api/databases/postgres/tables';
|
|
||||||
import type { PageServerLoad, RequestEvent } from './$types';
|
import type { PageServerLoad, RequestEvent } from './$types';
|
||||||
|
|
||||||
export const load: PageServerLoad = async (event) => {
|
export const load: PageServerLoad = async (event) => {
|
||||||
|
|
@ -18,54 +19,15 @@ export const load: PageServerLoad = async (event) => {
|
||||||
throw redirect(302, '/login', notSignedInMessage, event);
|
throw redirect(302, '/login', notSignedInMessage, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dbUser = await db.query.usersTable.findFirst({
|
const { data } = await locals.api.mfa.totp.$get().then(locals.parseApiResponse);
|
||||||
where: eq(usersTable.username, authedUser.username),
|
if (!data) {
|
||||||
});
|
throw redirect(302, '/login', notSignedInMessage, event);
|
||||||
|
|
||||||
const twoFactorDetails = await db.query.twoFactorTable.findFirst({
|
|
||||||
where: eq(twoFactorTable.userId, authedUser.id),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!twoFactorDetails || !twoFactorDetails.enabled) {
|
|
||||||
const message = {
|
|
||||||
type: 'error',
|
|
||||||
message: 'Two factor authentication is not enabled',
|
|
||||||
} as const;
|
|
||||||
redirect(302, '/login', message, event);
|
|
||||||
}
|
}
|
||||||
|
const { totpCredential } = data;
|
||||||
let twoFactorInitiatedTime = twoFactorDetails.initiatedTime;
|
if (!totpCredential) {
|
||||||
if (twoFactorInitiatedTime === null) {
|
throw redirect(302, '/login', notSignedInMessage, event);
|
||||||
console.log('twoFactorInitiatedTime is null');
|
|
||||||
twoFactorInitiatedTime = new Date();
|
|
||||||
console.log('twoFactorInitiatedTime', twoFactorInitiatedTime);
|
|
||||||
await db.update(twoFactorTable).set({ initiatedTime: twoFactorInitiatedTime }).where(eq(twoFactorTable.userId, dbUser!.id!));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if two factor started less than TWO_FACTOR_TIMEOUT
|
|
||||||
// const totpElapsed = totpTimeElapsed(twoFactorInitiatedTime)
|
|
||||||
// if (totpElapsed) {
|
|
||||||
// console.log('Time elapsed was more than TWO_FACTOR_TIMEOUT', totpElapsed, env.TWO_FACTOR_TIMEOUT)
|
|
||||||
// await lucia.invalidateSession(session!.id!)
|
|
||||||
// const sessionCookie = lucia.createBlankSessionCookie()
|
|
||||||
// cookies.set(sessionCookie.name, sessionCookie.value, {
|
|
||||||
// path: '.',
|
|
||||||
// ...sessionCookie.attributes,
|
|
||||||
// })
|
|
||||||
// const message = { type: 'error', message: 'Two factor authentication has expired' } as const
|
|
||||||
// redirect(302, '/login', message, event)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// const isTwoFactorAuthenticated = session?.isTwoFactorAuthenticated
|
|
||||||
//
|
|
||||||
// console.log('session', session)
|
|
||||||
// console.log('isTwoFactorAuthenticated', isTwoFactorAuthenticated)
|
|
||||||
|
|
||||||
// if (isTwoFactorAuthenticated && twoFactorDetails?.enabled && twoFactorDetails?.secret !== '') {
|
|
||||||
// const message = { type: 'success', message: 'You are already signed in' } as const
|
|
||||||
// throw redirect('/', message, event)
|
|
||||||
// }
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totpForm: await superValidate(event, zod(totpSchema)),
|
totpForm: await superValidate(event, zod(totpSchema)),
|
||||||
recoveryCodeForm: await superValidate(event, zod(recoveryCodeSchema)),
|
recoveryCodeForm: await superValidate(event, zod(recoveryCodeSchema)),
|
||||||
|
|
@ -81,70 +43,29 @@ export const actions: Actions = {
|
||||||
throw redirect(302, '/login', notSignedInMessage, event);
|
throw redirect(302, '/login', notSignedInMessage, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { dbUser, twoFactorDetails } = await validateUserData(event, locals);
|
const { data: totpData } = await locals.api.mfa.totp.$get().then(locals.parseApiResponse);
|
||||||
|
if (!totpData) {
|
||||||
|
throw redirect(302, '/login', notSignedInMessage, event);
|
||||||
|
}
|
||||||
|
const { totpCredential } = totpData;
|
||||||
|
if (!totpCredential) {
|
||||||
|
throw redirect(302, '/login', notSignedInMessage, event);
|
||||||
|
}
|
||||||
|
|
||||||
const totpForm = await superValidate(event, zod(totpSchema));
|
const totpForm = await superValidate(event, zod(totpSchema));
|
||||||
|
|
||||||
if (!totpForm.valid) {
|
if (!totpForm.valid) {
|
||||||
totpForm.data.totpToken = '';
|
totpForm.data.code = '';
|
||||||
return fail(400, { totpForm });
|
return fail(400, { totpForm });
|
||||||
}
|
}
|
||||||
|
|
||||||
// let sessionCookie
|
const { error: totpVerifyError } = await locals.api.mfa.totp.verify.$post({ json: { code: totpForm.data.code } }).then(locals.parseApiResponse);
|
||||||
// const totpToken = totpForm?.data?.totpToken
|
if (totpVerifyError) {
|
||||||
//
|
return setError(totpForm, 'code', totpVerifyError);
|
||||||
// const twoFactorSecretPopulated = twoFactorDetails.secret !== '' && twoFactorDetails.secret !== null
|
}
|
||||||
// if (twoFactorDetails.enabled && !twoFactorSecretPopulated && !totpToken) {
|
|
||||||
// return fail(400, { totpForm })
|
console.log('Successfully logged in');
|
||||||
// } else if (twoFactorSecretPopulated && totpToken) {
|
return message(totpForm, { type: 'success', message: 'Successfully logged in!' });
|
||||||
// // Check if two factor started less than TWO_FACTOR_TIMEOUT
|
|
||||||
// const totpElapsed = totpTimeElapsed(twoFactorDetails.initiatedTime ?? new Date())
|
|
||||||
// if (totpElapsed) {
|
|
||||||
// await lucia.invalidateSession(session!.id!)
|
|
||||||
// const sessionCookie = lucia.createBlankSessionCookie()
|
|
||||||
// cookies.set(sessionCookie.name, sessionCookie.value, {
|
|
||||||
// path: '.',
|
|
||||||
// ...sessionCookie.attributes,
|
|
||||||
// })
|
|
||||||
// const message = {
|
|
||||||
// type: 'error',
|
|
||||||
// message: 'Two factor authentication has expired',
|
|
||||||
// } as const
|
|
||||||
// redirect(302, '/login', message, event)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// console.log('totpToken', totpToken)
|
|
||||||
// const validOTP = await new TOTPController().verify(totpToken, decodeHex(twoFactorDetails.secret ?? ''))
|
|
||||||
// console.log('validOTP', validOTP)
|
|
||||||
//
|
|
||||||
// if (!validOTP) {
|
|
||||||
// console.log('invalid TOTP code')
|
|
||||||
// totpForm.data.totpToken = ''
|
|
||||||
// return setError(totpForm, 'totpToken', 'Invalid code.')
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// console.log('ip', locals.ip)
|
|
||||||
// console.log('country', locals.country)
|
|
||||||
// await lucia.invalidateSession(session.id)
|
|
||||||
// const newSession = await lucia.createSession(dbUser.id, {
|
|
||||||
// ip_country: locals.country,
|
|
||||||
// ip_address: locals.ip,
|
|
||||||
// twoFactorAuthEnabled: true,
|
|
||||||
// isTwoFactorAuthenticated: true,
|
|
||||||
// })
|
|
||||||
// console.log('logging in session', newSession)
|
|
||||||
// sessionCookie = lucia.createSessionCookie(newSession.id)
|
|
||||||
// console.log('logging in session cookie', sessionCookie)
|
|
||||||
//
|
|
||||||
// console.log('setting session cookie', sessionCookie)
|
|
||||||
// event.cookies.set(sessionCookie.name, sessionCookie.value, {
|
|
||||||
// path: '.',
|
|
||||||
// ...sessionCookie.attributes,
|
|
||||||
// })
|
|
||||||
//
|
|
||||||
// totpForm.data.totpToken = ''
|
|
||||||
// const message = { type: 'success', message: 'Signed In!' } as const
|
|
||||||
redirect(302, '/', message, event);
|
|
||||||
},
|
},
|
||||||
validateRecoveryCode: async (event) => {
|
validateRecoveryCode: async (event) => {
|
||||||
const { cookies, locals } = event;
|
const { cookies, locals } = event;
|
||||||
|
|
|
||||||
|
|
@ -40,20 +40,20 @@ const { form: recoveryCodeFormData, enhance: recoveryCodeEnhance } = superRecove
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
{#if !showRecoveryCode}
|
{#if !showRecoveryCode}
|
||||||
{@render totpForm()}
|
{@render totpForm()}
|
||||||
<Button variant="link" class="text-secondary-foreground" on:click={() => showRecoveryCode = true}>Show Recovery Code</Button>
|
<Button variant="link" class="text-secondary-foreground" on:click={() => showRecoveryCode = true}>Use Recovery Code</Button>
|
||||||
{:else}
|
{:else}
|
||||||
{@render recoveryCodeForm()}
|
{@render recoveryCodeForm()}
|
||||||
<Button variant="link" class="text-secondary-foreground" on:click={() => showRecoveryCode = false}>Show TOTP Code</Button>
|
<Button variant="link" class="text-secondary-foreground" on:click={() => showRecoveryCode = false}>Use TOTP Code</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|
||||||
{#snippet totpForm()}
|
{#snippet totpForm()}
|
||||||
<form method="POST" action="?/validateTotp" use:totpEnhance>
|
<form method="POST" action="?/validateTotp" use:totpEnhance>
|
||||||
<Form.Field class="form-field-container" form={superTotpForm} name="totpToken">
|
<Form.Field class="form-field-container" form={superTotpForm} name="code">
|
||||||
<Form.Control let:attrs>
|
<Form.Control let:attrs>
|
||||||
<Form.Label>TOTP Code</Form.Label>
|
<Form.Label>TOTP Code</Form.Label>
|
||||||
<PinInput {...attrs} bind:value={$totpFormData.totpToken} class="justify-evenly" />
|
<PinInput {...attrs} bind:value={$totpFormData.code} class="justify-evenly" />
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
<Form.FieldErrors />
|
<Form.FieldErrors />
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue