Refactoring the app layout, fixing checks for session and not user to fix totp.

This commit is contained in:
Bradley Shellnut 2024-11-23 14:49:16 -08:00
parent 6e67b2d4e1
commit 0f2344c70f
46 changed files with 748 additions and 724 deletions

View file

@ -54,7 +54,7 @@
"postcss-load-config": "^5.1.0", "postcss-load-config": "^5.1.0",
"postcss-preset-env": "^9.6.0", "postcss-preset-env": "^9.6.0",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.8", "prettier-plugin-svelte": "^3.3.0",
"svelte": "5.0.0-next.175", "svelte": "5.0.0-next.175",
"svelte-check": "^3.8.6", "svelte-check": "^3.8.6",
"svelte-headless-table": "^0.18.3", "svelte-headless-table": "^0.18.3",
@ -82,7 +82,7 @@
"@iconify-icons/line-md": "^1.2.30", "@iconify-icons/line-md": "^1.2.30",
"@iconify-icons/mdi": "^1.2.48", "@iconify-icons/mdi": "^1.2.48",
"@inlang/paraglide-sveltekit": "^0.11.1", "@inlang/paraglide-sveltekit": "^0.11.1",
"@internationalized/date": "^3.5.6", "@internationalized/date": "^3.6.0",
"@lucia-auth/adapter-drizzle": "^1.1.0", "@lucia-auth/adapter-drizzle": "^1.1.0",
"@lukeed/uuid": "^2.0.1", "@lukeed/uuid": "^2.0.1",
"@needle-di/core": "^0.8.4", "@needle-di/core": "^0.8.4",
@ -96,22 +96,22 @@
"@oslojs/otp": "^1.0.0", "@oslojs/otp": "^1.0.0",
"@oslojs/webauthn": "^1.0.0", "@oslojs/webauthn": "^1.0.0",
"@paralleldrive/cuid2": "^2.2.2", "@paralleldrive/cuid2": "^2.2.2",
"@scalar/hono-api-reference": "^0.5.159", "@scalar/hono-api-reference": "^0.5.160",
"@sveltejs/adapter-node": "^5.2.9", "@sveltejs/adapter-node": "^5.2.9",
"@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.27.0", "bullmq": "^5.28.1",
"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.2",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"dotenv-expand": "^11.0.7", "dotenv-expand": "^11.0.7",
"drizzle-orm": "^0.36.3", "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",
"hono": "^4.6.10", "hono": "^4.6.11",
"hono-pino": "^0.3.0", "hono-pino": "^0.3.0",
"hono-rate-limiter": "^0.4.0", "hono-rate-limiter": "^0.4.0",
"hono-zod-openapi": "^0.4.2", "hono-zod-openapi": "^0.4.2",

View file

@ -13,13 +13,13 @@ importers:
version: 5.1.0 version: 5.1.0
'@hono/swagger-ui': '@hono/swagger-ui':
specifier: ^0.4.1 specifier: ^0.4.1
version: 0.4.1(hono@4.6.10) version: 0.4.1(hono@4.6.11)
'@hono/zod-openapi': '@hono/zod-openapi':
specifier: ^0.15.3 specifier: ^0.15.3
version: 0.15.3(hono@4.6.10)(zod@3.23.8) version: 0.15.3(hono@4.6.11)(zod@3.23.8)
'@hono/zod-validator': '@hono/zod-validator':
specifier: ^0.2.2 specifier: ^0.2.2
version: 0.2.2(hono@4.6.10)(zod@3.23.8) version: 0.2.2(hono@4.6.11)(zod@3.23.8)
'@iconify-icons/line-md': '@iconify-icons/line-md':
specifier: ^1.2.30 specifier: ^1.2.30
version: 1.2.30 version: 1.2.30
@ -30,8 +30,8 @@ importers:
specifier: ^0.11.1 specifier: ^0.11.1
version: 0.11.5(@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: 0.11.5(@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)))
'@internationalized/date': '@internationalized/date':
specifier: ^3.5.6 specifier: ^3.6.0
version: 3.5.6 version: 3.6.0
'@lucia-auth/adapter-drizzle': '@lucia-auth/adapter-drizzle':
specifier: ^1.1.0 specifier: ^1.1.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) 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)
@ -72,8 +72,8 @@ importers:
specifier: ^2.2.2 specifier: ^2.2.2
version: 2.2.2 version: 2.2.2
'@scalar/hono-api-reference': '@scalar/hono-api-reference':
specifier: ^0.5.159 specifier: ^0.5.160
version: 0.5.159(hono@4.6.10) version: 0.5.160(hono@4.6.11)
'@sveltejs/adapter-node': '@sveltejs/adapter-node':
specifier: ^5.2.9 specifier: ^5.2.9
version: 5.2.9(@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: 5.2.9(@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)))
@ -87,8 +87,8 @@ importers:
specifier: ^1.9.1 specifier: ^1.9.1
version: 1.9.1 version: 1.9.1
bullmq: bullmq:
specifier: ^5.27.0 specifier: ^5.28.1
version: 5.27.0 version: 5.28.1
class-variance-authority: class-variance-authority:
specifier: ^0.7.0 specifier: ^0.7.0
version: 0.7.0 version: 0.7.0
@ -96,8 +96,8 @@ importers:
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1 version: 2.1.1
cookie: cookie:
specifier: ^1.0.1 specifier: ^1.0.2
version: 1.0.1 version: 1.0.2
dotenv: dotenv:
specifier: ^16.4.5 specifier: ^16.4.5
version: 16.4.5 version: 16.4.5
@ -117,17 +117,17 @@ importers:
specifier: ^4.7.8 specifier: ^4.7.8
version: 4.7.8 version: 4.7.8
hono: hono:
specifier: ^4.6.10 specifier: ^4.6.11
version: 4.6.10 version: 4.6.11
hono-pino: hono-pino:
specifier: ^0.3.0 specifier: ^0.3.0
version: 0.3.0(hono@4.6.10)(pino@9.5.0) version: 0.3.0(hono@4.6.11)(pino@9.5.0)
hono-rate-limiter: hono-rate-limiter:
specifier: ^0.4.0 specifier: ^0.4.0
version: 0.4.0(hono@4.6.10) version: 0.4.0(hono@4.6.11)
hono-zod-openapi: hono-zod-openapi:
specifier: ^0.4.2 specifier: ^0.4.2
version: 0.4.2(hono@4.6.10)(zod@3.23.8) version: 0.4.2(hono@4.6.11)(zod@3.23.8)
html-entities: html-entities:
specifier: ^2.5.2 specifier: ^2.5.2
version: 2.5.2 version: 2.5.2
@ -178,7 +178,7 @@ importers:
version: 0.2.2 version: 0.2.2
stoker: stoker:
specifier: ^1.3.0 specifier: ^1.3.0
version: 1.3.0(@asteasolutions/zod-to-openapi@7.1.2(zod@3.23.8))(@hono/zod-openapi@0.15.3(hono@4.6.10)(zod@3.23.8))(hono@4.6.10)(openapi3-ts@4.4.0) version: 1.3.0(@asteasolutions/zod-to-openapi@7.1.2(zod@3.23.8))(@hono/zod-openapi@0.15.3(hono@4.6.11)(zod@3.23.8))(hono@4.6.11)(openapi3-ts@4.4.0)
svelte-lazy-loader: svelte-lazy-loader:
specifier: ^1.0.0 specifier: ^1.0.0
version: 1.0.0 version: 1.0.0
@ -292,8 +292,8 @@ importers:
specifier: ^3.3.3 specifier: ^3.3.3
version: 3.3.3 version: 3.3.3
prettier-plugin-svelte: prettier-plugin-svelte:
specifier: ^3.2.8 specifier: ^3.3.0
version: 3.2.8(prettier@3.3.3)(svelte@5.0.0-next.175) version: 3.3.0(prettier@3.3.3)(svelte@5.0.0-next.175)
svelte: svelte:
specifier: 5.0.0-next.175 specifier: 5.0.0-next.175
version: 5.0.0-next.175 version: 5.0.0-next.175
@ -1665,8 +1665,8 @@ packages:
'@inlang/translatable@1.3.1': '@inlang/translatable@1.3.1':
resolution: {integrity: sha512-VAtle21vRpIrB+axtHFrFB0d1HtDaaNj+lV77eZQTJyOWbTFYTVIQJ8WAbyw9eu4F6h6QC2FutLyxjMomxfpcQ==} resolution: {integrity: sha512-VAtle21vRpIrB+axtHFrFB0d1HtDaaNj+lV77eZQTJyOWbTFYTVIQJ8WAbyw9eu4F6h6QC2FutLyxjMomxfpcQ==}
'@internationalized/date@3.5.6': '@internationalized/date@3.6.0':
resolution: {integrity: sha512-jLxQjefH9VI5P9UQuqB6qNKnvFt1Ky1TPIzHGsIlCi7sZZoMR8SdYbBGRvM0y+Jtb+ez4ieBzmiAUcpmPYpyOw==} resolution: {integrity: sha512-+z6ti+CcJnRlLHok/emGEsWQhe7kfSmEW+/6qCzvKY67YPh7YOBfvc7+/+NXq+zJlbArg30tYpqLjNgcAYv2YQ==}
'@ioredis/commands@1.2.0': '@ioredis/commands@1.2.0':
resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==}
@ -2344,8 +2344,8 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@scalar/hono-api-reference@0.5.159': '@scalar/hono-api-reference@0.5.160':
resolution: {integrity: sha512-nUKaN0CKvytbXPj9b6taF/efKKRqEUwhVxlfLVjrJXN0eHNHDWxG9e/5Tyw1o2MXJo1cQpGZ4qTh48k/8u6ZjA==} resolution: {integrity: sha512-3NXXh4B+5sa9QEchtQNKH0me4pEB8soFfCSqOxsuB0d8agokDO574cP9Qq4l6vwSh6FMiqSdaGTv5459g8hTCg==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
hono: ^4.0.0 hono: ^4.0.0
@ -2354,8 +2354,8 @@ packages:
resolution: {integrity: sha512-6geH9ehvQ/sG/xUyy3e0lyOw3BaY5s6nn22wHjEJhcobdmWyFER0O6m7AU0ZN4QTjle/gYvFJOjj552l/rsNSw==} resolution: {integrity: sha512-6geH9ehvQ/sG/xUyy3e0lyOw3BaY5s6nn22wHjEJhcobdmWyFER0O6m7AU0ZN4QTjle/gYvFJOjj552l/rsNSw==}
engines: {node: '>=18'} engines: {node: '>=18'}
'@scalar/types@0.0.19': '@scalar/types@0.0.20':
resolution: {integrity: sha512-wOxtXd35BS0DaVhBopQUB8c8hfLQ+/PKEr99GbOZW+4DWCrEB8JfWJgvpJyxHU6by7LHNVY4fvpFQR7Ezh1IIw==} resolution: {integrity: sha512-Sx7tqiuV9ZNp2XpIXKD/srzTjjb4I6aof0Y7+U5MgEuKPwrRnYS/BaocdNgWLnpCZisBIg5qp4YSqozxibabVg==}
engines: {node: '>=18'} engines: {node: '>=18'}
'@sideway/address@4.1.5': '@sideway/address@4.1.5':
@ -2801,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.27.0: bullmq@5.28.1:
resolution: {integrity: sha512-DZWrjDLkecZZ1/43h/SkG6CxU8nO/Lq/0svVoQdw33ksUCGfccgjbvCa/cuxHP/OvhxlTAA0cO3dBOoaT7sRFQ==} resolution: {integrity: sha512-iSoqziPLKH//mmoc4Aj3/opTmk1PgFdITwUrx/wDqrTxfBRjnTGInsu129LCEY6d+SmhZWnA9PYU6ciX+NT64A==}
bytes@3.1.2: bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
@ -2956,8 +2956,8 @@ packages:
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
cookie@1.0.1: cookie@1.0.2:
resolution: {integrity: sha512-Xd8lFX4LM9QEEwxQpF9J9NTUh8pmdJO0cyRJhFiDoLTk2eH8FXlRv2IFGYVadZpqI3j8fhNrSdKCeYPxiAhLXw==} resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
engines: {node: '>=18'} engines: {node: '>=18'}
core-js@3.38.1: core-js@3.38.1:
@ -3659,8 +3659,8 @@ packages:
hono: ^4.6.3 hono: ^4.6.3
zod: ^3.21.4 zod: ^3.21.4
hono@4.6.10: hono@4.6.11:
resolution: {integrity: sha512-IXXNfRAZEahFnWBhUUlqKEGF9upeE6hZoRZszvNkyAz/CYp+iVbxm3viMvStlagRJohjlBRGOQ7f4jfcV0XMGg==} resolution: {integrity: sha512-f0LwJQFKdUUrCUAVowxSvNCjyzI7ZLt8XWYU/EApyeq5FfOvHFarBaE5rjU9HTNFk4RI0FkdB2edb3p/7xZjzQ==}
engines: {node: '>=16.9.0'} engines: {node: '>=16.9.0'}
hookable@5.5.3: hookable@5.5.3:
@ -4648,8 +4648,8 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
prettier-plugin-svelte@3.2.8: prettier-plugin-svelte@3.3.0:
resolution: {integrity: sha512-PAHmmU5cGZdnhW4mWhmvxuG2PVbbHIxUuPOdUKvfE+d4Qt2d29iU5VWrPdsaW5YqVEE0nqhlvN4eoKmVMpIF3Q==} resolution: {integrity: sha512-iNoYiQUx4zwqbQDW/bk0WR75w+QiY4fHJQpGQ5v8Yr7X5m7YoSvs2buUnhoYFXNAL32ULVmrjPSc0vVOHJsO0Q==}
peerDependencies: peerDependencies:
prettier: ^3.0.0 prettier: ^3.0.0
svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0
@ -6325,25 +6325,25 @@ snapshots:
'@hapi/hoek': 9.3.0 '@hapi/hoek': 9.3.0
optional: true optional: true
'@hono/swagger-ui@0.4.1(hono@4.6.10)': '@hono/swagger-ui@0.4.1(hono@4.6.11)':
dependencies: dependencies:
hono: 4.6.10 hono: 4.6.11
'@hono/zod-openapi@0.15.3(hono@4.6.10)(zod@3.23.8)': '@hono/zod-openapi@0.15.3(hono@4.6.11)(zod@3.23.8)':
dependencies: dependencies:
'@asteasolutions/zod-to-openapi': 7.1.2(zod@3.23.8) '@asteasolutions/zod-to-openapi': 7.1.2(zod@3.23.8)
'@hono/zod-validator': 0.2.2(hono@4.6.10)(zod@3.23.8) '@hono/zod-validator': 0.2.2(hono@4.6.11)(zod@3.23.8)
hono: 4.6.10 hono: 4.6.11
zod: 3.23.8 zod: 3.23.8
'@hono/zod-validator@0.2.2(hono@4.6.10)(zod@3.23.8)': '@hono/zod-validator@0.2.2(hono@4.6.11)(zod@3.23.8)':
dependencies: dependencies:
hono: 4.6.10 hono: 4.6.11
zod: 3.23.8 zod: 3.23.8
'@hono/zod-validator@0.4.1(hono@4.6.10)(zod@3.23.8)': '@hono/zod-validator@0.4.1(hono@4.6.11)(zod@3.23.8)':
dependencies: dependencies:
hono: 4.6.10 hono: 4.6.11
zod: 3.23.8 zod: 3.23.8
'@humanwhocodes/config-array@0.13.0': '@humanwhocodes/config-array@0.13.0':
@ -6571,7 +6571,7 @@ snapshots:
dependencies: dependencies:
'@inlang/language-tag': 1.5.1 '@inlang/language-tag': 1.5.1
'@internationalized/date@3.5.6': '@internationalized/date@3.6.0':
dependencies: dependencies:
'@swc/helpers': 0.5.13 '@swc/helpers': 0.5.13
@ -6665,7 +6665,7 @@ snapshots:
dependencies: dependencies:
'@floating-ui/core': 1.6.8 '@floating-ui/core': 1.6.8
'@floating-ui/dom': 1.6.11 '@floating-ui/dom': 1.6.11
'@internationalized/date': 3.5.6 '@internationalized/date': 3.6.0
dequal: 2.0.3 dequal: 2.0.3
focus-trap: 7.6.0 focus-trap: 7.6.0
nanoid: 5.0.7 nanoid: 5.0.7
@ -6675,7 +6675,7 @@ snapshots:
dependencies: dependencies:
'@floating-ui/core': 1.6.8 '@floating-ui/core': 1.6.8
'@floating-ui/dom': 1.6.11 '@floating-ui/dom': 1.6.11
'@internationalized/date': 3.5.6 '@internationalized/date': 3.6.0
dequal: 2.0.3 dequal: 2.0.3
focus-trap: 7.6.0 focus-trap: 7.6.0
nanoid: 5.0.7 nanoid: 5.0.7
@ -7226,14 +7226,14 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.24.0': '@rollup/rollup-win32-x64-msvc@4.24.0':
optional: true optional: true
'@scalar/hono-api-reference@0.5.159(hono@4.6.10)': '@scalar/hono-api-reference@0.5.160(hono@4.6.11)':
dependencies: dependencies:
'@scalar/types': 0.0.19 '@scalar/types': 0.0.20
hono: 4.6.10 hono: 4.6.11
'@scalar/openapi-types@0.1.5': {} '@scalar/openapi-types@0.1.5': {}
'@scalar/types@0.0.19': '@scalar/types@0.0.20':
dependencies: dependencies:
'@scalar/openapi-types': 0.1.5 '@scalar/openapi-types': 0.1.5
'@unhead/schema': 1.11.11 '@unhead/schema': 1.11.11
@ -7701,7 +7701,7 @@ snapshots:
bits-ui@0.21.16(svelte@5.0.0-next.175): bits-ui@0.21.16(svelte@5.0.0-next.175):
dependencies: dependencies:
'@internationalized/date': 3.5.6 '@internationalized/date': 3.6.0
'@melt-ui/svelte': 0.76.2(svelte@5.0.0-next.175) '@melt-ui/svelte': 0.76.2(svelte@5.0.0-next.175)
nanoid: 5.0.7 nanoid: 5.0.7
svelte: 5.0.0-next.175 svelte: 5.0.0-next.175
@ -7766,7 +7766,7 @@ snapshots:
base64-js: 1.5.1 base64-js: 1.5.1
ieee754: 1.2.1 ieee754: 1.2.1
bullmq@5.27.0: bullmq@5.28.1:
dependencies: dependencies:
cron-parser: 4.9.0 cron-parser: 4.9.0
ioredis: 5.4.1 ioredis: 5.4.1
@ -7921,7 +7921,7 @@ snapshots:
cookie@0.6.0: {} cookie@0.6.0: {}
cookie@1.0.1: {} cookie@1.0.2: {}
core-js@3.38.1: {} core-js@3.38.1: {}
@ -8636,24 +8636,24 @@ snapshots:
help-me@5.0.0: {} help-me@5.0.0: {}
hono-pino@0.3.0(hono@4.6.10)(pino@9.5.0): hono-pino@0.3.0(hono@4.6.11)(pino@9.5.0):
dependencies: dependencies:
defu: 6.1.4 defu: 6.1.4
hono: 4.6.10 hono: 4.6.11
pino: 9.5.0 pino: 9.5.0
hono-rate-limiter@0.4.0(hono@4.6.10): hono-rate-limiter@0.4.0(hono@4.6.11):
dependencies: dependencies:
hono: 4.6.10 hono: 4.6.11
hono-zod-openapi@0.4.2(hono@4.6.10)(zod@3.23.8): hono-zod-openapi@0.4.2(hono@4.6.11)(zod@3.23.8):
dependencies: dependencies:
'@hono/zod-validator': 0.4.1(hono@4.6.10)(zod@3.23.8) '@hono/zod-validator': 0.4.1(hono@4.6.11)(zod@3.23.8)
hono: 4.6.10 hono: 4.6.11
zod: 3.23.8 zod: 3.23.8
zod-openapi: 3.3.0(zod@3.23.8) zod-openapi: 3.3.0(zod@3.23.8)
hono@4.6.10: {} hono@4.6.11: {}
hookable@5.5.3: {} hookable@5.5.3: {}
@ -9634,7 +9634,7 @@ snapshots:
prelude-ls@1.2.1: {} prelude-ls@1.2.1: {}
prettier-plugin-svelte@3.2.8(prettier@3.3.3)(svelte@5.0.0-next.175): prettier-plugin-svelte@3.3.0(prettier@3.3.3)(svelte@5.0.0-next.175):
dependencies: dependencies:
prettier: 3.3.3 prettier: 3.3.3
svelte: 5.0.0-next.175 svelte: 5.0.0-next.175
@ -9962,13 +9962,13 @@ snapshots:
std-env@3.7.0: {} std-env@3.7.0: {}
stoker@1.3.0(@asteasolutions/zod-to-openapi@7.1.2(zod@3.23.8))(@hono/zod-openapi@0.15.3(hono@4.6.10)(zod@3.23.8))(hono@4.6.10)(openapi3-ts@4.4.0): stoker@1.3.0(@asteasolutions/zod-to-openapi@7.1.2(zod@3.23.8))(@hono/zod-openapi@0.15.3(hono@4.6.11)(zod@3.23.8))(hono@4.6.11)(openapi3-ts@4.4.0):
dependencies: dependencies:
'@asteasolutions/zod-to-openapi': 7.1.2(zod@3.23.8) '@asteasolutions/zod-to-openapi': 7.1.2(zod@3.23.8)
hono: 4.6.10 hono: 4.6.11
openapi3-ts: 4.4.0 openapi3-ts: 4.4.0
optionalDependencies: optionalDependencies:
'@hono/zod-openapi': 0.15.3(hono@4.6.10)(zod@3.23.8) '@hono/zod-openapi': 0.15.3(hono@4.6.11)(zod@3.23.8)
string-width@4.2.3: string-width@4.2.3:
dependencies: dependencies:

5
src/app.d.ts vendored
View file

@ -1,5 +1,6 @@
import type { ApiClient } from '$lib/server/api'; import type { ApiClient } from '$lib/server/api';
import type { Users } from '$lib/server/api/databases/postgres/tables'; import type { Users } from '$lib/server/api/databases/postgres/tables';
import type { Session } from '$lib/server/api/services/sessions.service';
import type { parseApiResponse } from '$lib/utils/api'; import type { parseApiResponse } from '$lib/utils/api';
// See https://svelte.dev/docs/kit/types#app.d.ts // See https://svelte.dev/docs/kit/types#app.d.ts
@ -16,8 +17,8 @@ declare global {
interface Locals { interface Locals {
api: ApiClient['api']; api: ApiClient['api'];
parseApiResponse: typeof parseApiResponse; parseApiResponse: typeof parseApiResponse;
getAuthedUser: () => Promise<Returned<Users> | null>; getAuthedUser: () => Promise<Returned<Record<Users, Session>> | null>;
getAuthedUserOrThrow: () => Promise<Returned<User>>; getAuthedUserOrThrow: () => Promise<Returned<Record<Users, Session>>>;
} }
namespace Superforms { namespace Superforms {
type Message = { type Message = {

View file

@ -21,12 +21,12 @@ const apiClient: Handle = async ({ event, resolve }) => {
/* ----------------------------- Auth functions ----------------------------- */ /* ----------------------------- Auth functions ----------------------------- */
async function getAuthedUser() { async function getAuthedUser() {
const { data } = await api.user.$get().then(parseApiResponse); const { data } = await api.me.$get().then(parseApiResponse);
return data?.user; return { user: data?.user, session: data?.session };
} }
async function getAuthedUserOrThrow() { async function getAuthedUserOrThrow() {
const { data } = await api.user.$get().then(parseApiResponse); const { data } = await api.me.$get().then(parseApiResponse);
if (!data || !data.user) { if (!data || !data.user) {
throw redirect(StatusCodes.TEMPORARY_REDIRECT, "/"); throw redirect(StatusCodes.TEMPORARY_REDIRECT, "/");
} }

View file

@ -1,4 +1,3 @@
import type { Sessions } from '$lib/server/api/databases/postgres/tables';
import type { Session } from '$lib/server/api/services/sessions.service'; import type { Session } from '$lib/server/api/services/sessions.service';
import type { Hono } from 'hono'; import type { Hono } from 'hono';
import type { PinoLogger } from 'hono-pino'; import type { PinoLogger } from 'hono-pino';

View file

@ -1,39 +1,39 @@
import {StatusCodes} from '$lib/constants/status-codes'; import { StatusCodes } from '$lib/constants/status-codes';
import {Controller} from '$lib/server/api/common/types/controller'; import { Controller } from '$lib/server/api/common/types/controller';
import {allCollections, getCollectionByCUID, numberOfCollections} from '$lib/server/api/controllers/collection.routes'; import { allCollections, getCollectionByCUID, numberOfCollections } from '$lib/server/api/controllers/collection.routes';
import {CollectionsService} from '$lib/server/api/services/collections.service'; import { CollectionsService } from '$lib/server/api/services/collections.service';
import {openApi} from 'hono-zod-openapi'; import { openApi } from 'hono-zod-openapi';
import { injectable, inject } from '@needle-di/core'; import { injectable, inject } from '@needle-di/core';
import {requireAuth} from '../middleware/require-auth.middleware'; import { requireFullAuth } from '../middleware/require-auth.middleware';
@injectable() @injectable()
export class CollectionController extends Controller { export class CollectionController extends Controller {
constructor(private collectionsService = inject(CollectionsService)) { constructor(private collectionsService = inject(CollectionsService)) {
super(); super();
} }
routes() { routes() {
return this.controller return this.controller
.get('/', requireAuth, openApi(allCollections), async (c) => { .get('/', requireFullAuth, openApi(allCollections), async (c) => {
const user = c.var.user; const user = c.var.user;
const collections = await this.collectionsService.findAllByUserId(user.id); const collections = await this.collectionsService.findAllByUserId(user.id);
console.log('collections service', collections); console.log('collections service', collections);
return c.json({ collections }, StatusCodes.OK); return c.json({ collections }, StatusCodes.OK);
}) })
.get('/count', requireAuth, openApi(numberOfCollections), async (c) => { .get('/count', requireFullAuth, openApi(numberOfCollections), async (c) => {
const user = c.var.user; const user = c.var.user;
const collections = await this.collectionsService.findAllByUserIdWithDetails(user.id); const collections = await this.collectionsService.findAllByUserIdWithDetails(user.id);
return c.json({ count: collections?.length || 0 }, StatusCodes.OK); return c.json({ count: collections?.length || 0 }, StatusCodes.OK);
}) })
.get('/:cuid', requireAuth, openApi(getCollectionByCUID), async (c) => { .get('/:cuid', requireFullAuth, openApi(getCollectionByCUID), async (c) => {
const cuid = c.req.param('cuid'); const cuid = c.req.param('cuid');
const collection = await this.collectionsService.findOneByCuid(cuid); const collection = await this.collectionsService.findOneByCuid(cuid);
if (!collection) { if (!collection) {
return c.json('Collection not found', StatusCodes.NOT_FOUND); return c.json('Collection not found', StatusCodes.NOT_FOUND);
} }
return c.json({ collection }, StatusCodes.OK); return c.json({ collection }, StatusCodes.OK);
}); });
} }
} }

View file

@ -1,116 +1,119 @@
import {StatusCodes} from '$lib/constants/status-codes'; import { StatusCodes } from '$lib/constants/status-codes';
import {Controller} from '$lib/server/api/common/types/controller'; import { Controller } from '$lib/server/api/common/types/controller';
import {createBlankSessionTokenCookie, setSessionCookie} from '$lib/server/api/common/utils/cookies'; import { createBlankSessionTokenCookie, setSessionCookie } from '$lib/server/api/common/utils/cookies';
import {changePasswordDto} from '$lib/server/api/dtos/change-password.dto'; import { changePasswordDto } from '$lib/server/api/dtos/change-password.dto';
import {updateEmailDto} from '$lib/server/api/dtos/update-email.dto'; import { updateEmailDto } from '$lib/server/api/dtos/update-email.dto';
import {updateProfileDto} from '$lib/server/api/dtos/update-profile.dto'; import { updateProfileDto } from '$lib/server/api/dtos/update-profile.dto';
import {verifyPasswordDto} from '$lib/server/api/dtos/verify-password.dto'; import { verifyPasswordDto } from '$lib/server/api/dtos/verify-password.dto';
import {limiter} from '$lib/server/api/middleware/rate-limiter.middleware'; import { limiter } from '$lib/server/api/middleware/rate-limiter.middleware';
import {IamService} from '$lib/server/api/services/iam.service'; import { IamService } from '$lib/server/api/services/iam.service';
import {LoginRequestsService} from '$lib/server/api/services/loginrequest.service'; import { LoginRequestsService } from '$lib/server/api/services/loginrequest.service';
import {SessionsService} from '$lib/server/api/services/sessions.service'; import { SessionsService } from '$lib/server/api/services/sessions.service';
import {zValidator} from '@hono/zod-validator'; import { zValidator } from '@hono/zod-validator';
import {openApi} from 'hono-zod-openapi'; import { openApi } from 'hono-zod-openapi';
import { injectable, inject } from "@needle-di/core"; import { injectable, inject } from '@needle-di/core';
import {requireAuth} from '../middleware/require-auth.middleware'; import { requireFullAuth, requireTempAuth } from '../middleware/require-auth.middleware';
import {iam, logout, updateEmail, updatePassword, updateProfile, verifyPassword} from './iam.routes'; import { iam, logout, updateEmail, updatePassword, updateProfile, verifyPassword } from './iam.routes';
import { UsersRepository } from '../repositories/users.repository';
@injectable() @injectable()
export class IamController extends Controller { export class IamController extends Controller {
constructor( constructor(
private iamService = inject(IamService), private iamService = inject(IamService),
private loginRequestService = inject(LoginRequestsService), private loginRequestService = inject(LoginRequestsService),
private sessionsService = inject(SessionsService), private sessionsService = inject(SessionsService),
) { private usersRepository = inject(UsersRepository),
super(); ) {
} super();
}
routes() { routes() {
return this.controller return this.controller
.get('/', requireAuth, openApi(iam), async (c) => { .get('/', openApi(iam), async (c) => {
const user = c.var.user; const session = c.var.session;
return c.json({ user }); const user = session ? await this.usersRepository.findOneByIdOrThrow(session.userId) : null;
}) return c.json({ user, session });
.put( })
'/update/profile', .put(
requireAuth, '/update/profile',
openApi(updateProfile), requireFullAuth,
zValidator('json', updateProfileDto), openApi(updateProfile),
limiter({ limit: 30, minutes: 60 }), zValidator('json', updateProfileDto),
async (c) => { limiter({ limit: 30, minutes: 60 }),
const user = c.var.user; async (c) => {
const { firstName, lastName, username } = c.req.valid('json'); const user = c.var.user;
const updatedUser = await this.iamService.updateProfile(user.id, { firstName, lastName, username }); const { firstName, lastName, username } = c.req.valid('json');
if (!updatedUser) { const updatedUser = await this.iamService.updateProfile(user.id, { firstName, lastName, username });
return c.json('Username already in use', StatusCodes.UNPROCESSABLE_ENTITY); if (!updatedUser) {
} return c.json('Username already in use', StatusCodes.UNPROCESSABLE_ENTITY);
return c.json({ user: updatedUser }, StatusCodes.OK); }
}, return c.json({ user: updatedUser }, StatusCodes.OK);
) },
.post( )
'/verify/password', .post(
requireAuth, '/verify/password',
zValidator('json', verifyPasswordDto), requireFullAuth,
openApi(verifyPassword), zValidator('json', verifyPasswordDto),
limiter({ limit: 10, minutes: 60 }), openApi(verifyPassword),
async (c) => { limiter({ limit: 10, minutes: 60 }),
const user = c.var.user; async (c) => {
const { password } = c.req.valid('json'); const user = c.var.user;
const passwordVerified = await this.iamService.verifyPassword(user.id, { password }); const { password } = c.req.valid('json');
if (!passwordVerified) { const passwordVerified = await this.iamService.verifyPassword(user.id, { password });
console.log('Incorrect password'); if (!passwordVerified) {
return c.json('Incorrect password', StatusCodes.FORBIDDEN); console.log('Incorrect password');
} return c.json('Incorrect password', StatusCodes.FORBIDDEN);
return c.json({}, StatusCodes.OK); }
}, return c.json({}, StatusCodes.OK);
) },
.put( )
'/update/password', .put(
requireAuth, '/update/password',
openApi(updatePassword), requireFullAuth,
zValidator('json', changePasswordDto), openApi(updatePassword),
limiter({ limit: 10, minutes: 60 }), zValidator('json', changePasswordDto),
async (c) => { limiter({ limit: 10, minutes: 60 }),
const user = c.var.user; async (c) => {
const { password, confirm_password } = c.req.valid('json'); const user = c.var.user;
if (password !== confirm_password) { const { password, confirm_password } = c.req.valid('json');
return c.json('Passwords do not match', StatusCodes.UNPROCESSABLE_ENTITY); if (password !== confirm_password) {
} return c.json('Passwords do not match', StatusCodes.UNPROCESSABLE_ENTITY);
try { }
await this.iamService.updatePassword(user.id, { password, confirm_password }); try {
await this.sessionsService.invalidateSession(user.id); await this.iamService.updatePassword(user.id, { password, confirm_password });
await this.loginRequestService.createUserSession(user.id, c.req, false); await this.sessionsService.invalidateSession(user.id);
const sessionCookie = createBlankSessionTokenCookie(); await this.loginRequestService.createUserSession(user.id, c.req, false, false);
setSessionCookie(c, sessionCookie); const sessionCookie = createBlankSessionTokenCookie();
return c.json({ status: 'success' }); setSessionCookie(c, sessionCookie);
} catch (error) { return c.json({ status: 'success' });
console.error('Error updating password', error); } catch (error) {
return c.json('Error updating password', StatusCodes.INTERNAL_SERVER_ERROR); console.error('Error updating password', error);
} return c.json('Error updating password', StatusCodes.INTERNAL_SERVER_ERROR);
}, }
) },
.post( )
'/update/email', .post(
requireAuth, '/update/email',
openApi(updateEmail), requireFullAuth,
zValidator('json', updateEmailDto), openApi(updateEmail),
limiter({ limit: 10, minutes: 60 }), zValidator('json', updateEmailDto),
async (c) => { limiter({ limit: 10, minutes: 60 }),
const user = c.var.user; async (c) => {
const { email } = c.req.valid('json'); const user = c.var.user;
const updatedUser = await this.iamService.updateEmail(user.id, { email }); const { email } = c.req.valid('json');
if (!updatedUser) { const updatedUser = await this.iamService.updateEmail(user.id, { email });
return c.json('Cannot change email address', StatusCodes.FORBIDDEN); if (!updatedUser) {
} return c.json('Cannot change email address', StatusCodes.FORBIDDEN);
return c.json({ user: updatedUser }, StatusCodes.OK); }
}, return c.json({ user: updatedUser }, StatusCodes.OK);
) },
.post('/logout', requireAuth, openApi(logout), async (c) => { )
const sessionId = c.var.session.id; .post('/logout', requireFullAuth, openApi(logout), async (c) => {
await this.iamService.logout(sessionId); const sessionId = c.var.session.id;
const sessionCookie = createBlankSessionTokenCookie(); await this.iamService.logout(sessionId);
setSessionCookie(c, sessionCookie); const sessionCookie = createBlankSessionTokenCookie();
return c.json({ status: 'success' }); setSessionCookie(c, sessionCookie);
}); return c.json({ status: 'success' });
} });
}
} }

View file

@ -1,37 +1,37 @@
import {Controller} from '$lib/server/api/common/types/controller'; import { Controller } from '$lib/server/api/common/types/controller';
import {cookieExpiresAt, createSessionTokenCookie, setSessionCookie} from '$lib/server/api/common/utils/cookies'; import { cookieExpiresAt, createSessionTokenCookie, setSessionCookie } from '$lib/server/api/common/utils/cookies';
import {signinUsernameDto} from '$lib/server/api/dtos/signin-username.dto'; import { signinUsernameDto } from '$lib/server/api/dtos/signin-username.dto';
import {SessionsService} from '$lib/server/api/services/sessions.service'; import { SessionsService } from '$lib/server/api/services/sessions.service';
import {zValidator} from '@hono/zod-validator'; import { zValidator } from '@hono/zod-validator';
import {openApi} from 'hono-zod-openapi'; import { openApi } from 'hono-zod-openapi';
import { inject, injectable } from '@needle-di/core'; import { inject, injectable } from '@needle-di/core';
import {limiter} from '../middleware/rate-limiter.middleware'; import { limiter } from '../middleware/rate-limiter.middleware';
import {LoginRequestsService} from '../services/loginrequest.service'; import { LoginRequestsService } from '../services/loginrequest.service';
import {signinUsername} from './login.routes'; import { signinUsername } from './login.routes';
@injectable() @injectable()
export class LoginController extends Controller { export class LoginController extends Controller {
constructor( constructor(
private loginRequestsService = inject(LoginRequestsService), private loginRequestsService = inject(LoginRequestsService),
private sessionsService = inject(SessionsService), private sessionsService = inject(SessionsService),
) { ) {
super(); super();
} }
routes() { routes() {
return this.controller.post( return this.controller.post(
'/', '/',
openApi(signinUsername), openApi(signinUsername),
zValidator('json', signinUsernameDto), zValidator('json', signinUsernameDto),
limiter({ limit: 10, minutes: 60 }), limiter({ limit: 10, minutes: 60 }),
async (c) => { async (c) => {
const { username, password } = c.req.valid('json'); const { username, password } = c.req.valid('json');
const session = await this.loginRequestsService.verify({ username, password }, c.req); const session = await this.loginRequestsService.verify({ username, password }, c.req);
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);
return c.json({ message: 'ok' }); return c.json({ message: 'ok' });
}, },
); );
} }
} }

View file

@ -7,7 +7,7 @@ import { UsersService } from '$lib/server/api/services/users.service';
import { zValidator } from '@hono/zod-validator'; 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 { requireFullAuth, requireTempAuth } from '../middleware/require-auth.middleware';
import { createTwoFactorSchema } from '../dtos/create-totp.dto'; import { createTwoFactorSchema } from '../dtos/create-totp.dto';
import { decodeBase64 } from '@oslojs/encoding'; import { decodeBase64 } from '@oslojs/encoding';
import { LoginRequestsService } from '../services/loginrequest.service'; import { LoginRequestsService } from '../services/loginrequest.service';
@ -26,12 +26,13 @@ export class MfaController extends Controller {
routes() { routes() {
return this.controller return this.controller
.get('/totp', requireAuth, async (c) => { .get('/totp', requireTempAuth, async (c) => {
const user = c.var.user; const user = c.var.user;
c.var.logger.info(`The user ${user.id} is requesting TOTP credentials`);
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, zValidator('json', createTwoFactorSchema), async (c) => { .post('/totp', requireTempAuth, zValidator('json', createTwoFactorSchema), async (c) => {
const user = c.var.user; const user = c.var.user;
const { key } = c.req.valid('json'); const { key } = c.req.valid('json');
const totpCredential = await this.totpService.create(user.id, decodeBase64(key)); const totpCredential = await this.totpService.create(user.id, decodeBase64(key));
@ -41,7 +42,7 @@ export class MfaController extends Controller {
} }
return c.status(StatusCodes.INTERNAL_SERVER_ERROR); return c.status(StatusCodes.INTERNAL_SERVER_ERROR);
}) })
.delete('/totp', requireAuth, async (c) => { .delete('/totp', requireFullAuth, async (c) => {
const user = c.var.user; const user = c.var.user;
try { try {
await this.totpService.deleteOneByUserIdAndType(user.id, CredentialsType.TOTP); await this.totpService.deleteOneByUserIdAndType(user.id, CredentialsType.TOTP);
@ -54,20 +55,20 @@ export class MfaController extends Controller {
return c.status(StatusCodes.INTERNAL_SERVER_ERROR); return c.status(StatusCodes.INTERNAL_SERVER_ERROR);
} }
}) })
.get('/totp/recoveryCodes', requireAuth, async (c) => { .get('/totp/recoveryCodes', requireFullAuth, async (c) => {
const user = c.var.user; const user = c.var.user;
// You can only view recovery codes once and that is on creation // You can only view recovery codes once and that is on creation
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);
// Filter out codes that are not used and only return the code // Filter out codes that are not used and only return the code
const codes = existingCodes.filter(code => !code.used).map(code => code.code); const codes = existingCodes.filter((code) => !code.used).map((code) => code.code);
return c.json({ recoveryCodes: codes }); 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) => { .post('/totp/recoveryCodes', requireFullAuth, 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');
@ -82,7 +83,7 @@ export class MfaController extends Controller {
return c.status(StatusCodes.INTERNAL_SERVER_ERROR); return c.status(StatusCodes.INTERNAL_SERVER_ERROR);
} }
}) })
.post('/totp/verify', requireAuth, zValidator('json', verifyTotpDto), async (c) => { .post('/totp/verify', requireTempAuth, 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');
@ -90,7 +91,7 @@ export class MfaController extends Controller {
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 session = await this.loginRequestService.createUserSession(user.id, c.req, true, true);
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);

View file

@ -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, false); const session = await this.loginRequestService.createUserSession(user.id, c.req, false, 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);

View file

@ -1,29 +1,30 @@
import {Controller} from '$lib/server/api/common/types/controller'; import { Controller } from '$lib/server/api/common/types/controller';
import {UsersService} from '$lib/server/api/services/users.service'; import { UsersService } from '$lib/server/api/services/users.service';
import {inject, injectable} from '@needle-di/core'; import { inject, injectable } from '@needle-di/core';
import {requireAuth} from '../middleware/require-auth.middleware'; import { requireFullAuth, requireTempAuth } from '../middleware/require-auth.middleware';
@injectable() @injectable()
export class UserController extends Controller { export class UserController extends Controller {
constructor(private usersService = inject(UsersService)) { constructor(private usersService = inject(UsersService)) {
super(); super();
} }
routes() { routes() {
return this.controller return this.controller
.get('/', async (c) => { .get('/', requireTempAuth, async (c) => {
const user = c.var.user; const session = c.var.session;
return c.json({ user }); const user = session ? await this.usersService.findOneById(session.userId) : null;
}) return c.json({ user, session });
.get('/:id', requireAuth, async (c) => { })
const id = c.req.param('id'); .get('/:id', requireFullAuth, async (c) => {
const user = await this.usersService.findOneById(id); const id = c.req.param('id');
return c.json({ user }); const user = await this.usersService.findOneById(id);
}) return c.json({ user });
.get('/username/:userName', requireAuth, async (c) => { })
const userName = c.req.param('userName'); .get('/username/:userName', requireFullAuth, async (c) => {
const user = await this.usersService.findOneByUsername(userName); const userName = c.req.param('userName');
return c.json({ user }); const user = await this.usersService.findOneByUsername(userName);
}); return c.json({ user });
} });
}
} }

View file

@ -1,25 +1,25 @@
import {Controller} from '$lib/server/api/common/types/controller' import { Controller } from '$lib/server/api/common/types/controller';
import {WishlistsService} from '$lib/server/api/services/wishlists.service' import { WishlistsService } from '$lib/server/api/services/wishlists.service';
import {inject, injectable} from '@needle-di/core' import { inject, injectable } from '@needle-di/core';
import {requireAuth} from '../middleware/require-auth.middleware' import { requireFullAuth } from '../middleware/require-auth.middleware';
@injectable() @injectable()
export class WishlistController extends Controller { export class WishlistController extends Controller {
constructor(private wishlistsService = inject(WishlistsService)) { constructor(private wishlistsService = inject(WishlistsService)) {
super() super();
} }
routes() { routes() {
return this.controller return this.controller
.get('/', requireAuth, async (c) => { .get('/', requireFullAuth, async (c) => {
const user = c.var.user const user = c.var.user;
const wishlists = await this.wishlistsService.findAllByUserId(user.id) const wishlists = await this.wishlistsService.findAllByUserId(user.id);
return c.json({ wishlists }) return c.json({ wishlists });
}) })
.get('/:cuid', requireAuth, async (c) => { .get('/:cuid', requireFullAuth, async (c) => {
const cuid = c.req.param('cuid') const cuid = c.req.param('cuid');
const wishlist = await this.wishlistsService.findOneByCuid(cuid) const wishlist = await this.wishlistsService.findOneByCuid(cuid);
return c.json({ wishlist }) return c.json({ wishlist });
}) });
} }
} }

View file

@ -36,7 +36,6 @@ for (const table of [
schema.publishers_to_games, schema.publishers_to_games,
schema.recoveryCodesTable, schema.recoveryCodesTable,
schema.rolesTable, schema.rolesTable,
schema.sessionsTable,
schema.twoFactorTable, schema.twoFactorTable,
schema.user_roles, schema.user_roles,
schema.usersTable, schema.usersTable,

View file

@ -18,7 +18,6 @@ export * from './publishersToExternalIds.table'
export * from './publishersToGames.table' export * from './publishersToGames.table'
export * from './recovery-codes.table' export * from './recovery-codes.table'
export * from './roles.table' export * from './roles.table'
export * from './sessions.table'
export * from './two-factor.table' export * from './two-factor.table'
export * from './userRoles.table' export * from './userRoles.table'
export * from './users.table' export * from './users.table'

View file

@ -1,28 +0,0 @@
import {type InferSelectModel, relations} from 'drizzle-orm';
import {boolean, pgTable, text, timestamp, uuid} from 'drizzle-orm/pg-core';
import {cuid2} from '../../../common/utils/table';
import {usersTable} from './users.table';
export const sessionsTable = pgTable('sessions', {
id: cuid2().primaryKey(),
userId: uuid()
.notNull()
.references(() => usersTable.id, { onDelete: 'cascade' }),
expiresAt: timestamp({
withTimezone: true,
mode: 'date',
}).notNull(),
ipCountry: text(),
ipAddress: text(),
twoFactorAuthEnabled: boolean().default(false),
isTwoFactorAuthenticated: boolean().default(false),
});
export const sessionsRelations = relations(sessionsTable, ({ one }) => ({
user: one(usersTable, {
fields: [sessionsTable.userId],
references: [usersTable.id],
}),
}));
export type Sessions = InferSelectModel<typeof sessionsTable>;

View file

@ -1,16 +1,30 @@
import {Unauthorized} from '$lib/server/api/common/exceptions'; import { Unauthorized } from '$lib/server/api/common/exceptions';
import type {Sessions} from '$lib/server/api/databases/postgres/tables'; import type { Sessions, Users } from '$lib/server/api/databases/postgres/tables';
import type {MiddlewareHandler} from 'hono'; import type { MiddlewareHandler } from 'hono';
import {createMiddleware} from 'hono/factory'; import { createMiddleware } from 'hono/factory';
import type {User} from 'lucia';
export const requireAuth: MiddlewareHandler<{ export const requireFullAuth: MiddlewareHandler<{
Variables: {
session: Sessions;
user: Users;
};
}> = createMiddleware(async (c, next) => {
const session = c.var.session;
if (!session || (session?.twoFactorAuthEnabled && !session?.twoFactorVerified)) {
throw Unauthorized('You must be logged in to access this resource');
}
return next();
});
export const requireTempAuth: MiddlewareHandler<{
Variables: { Variables: {
session: Sessions; session: Sessions;
user: User; user: Users;
}; };
}> = createMiddleware(async (c, next) => { }> = createMiddleware(async (c, next) => {
const user = c.var.user; const session = c.var.session;
if (!user) throw Unauthorized('You must be logged in to access this resource'); if (!session) {
throw Unauthorized('You must be logged in to access this resource');
}
return next(); return next();
}); });

View file

@ -2,7 +2,7 @@ import type { SigninUsernameDto } from '$lib/server/api/dtos/signin-username.dto
import { SessionsService } from '$lib/server/api/services/sessions.service'; import { SessionsService } from '$lib/server/api/services/sessions.service';
import type { HonoRequest } from 'hono'; import type { HonoRequest } from 'hono';
import { inject, injectable } from '@needle-di/core'; import { inject, injectable } from '@needle-di/core';
import { BadRequest } from '../common/exceptions'; import { BadRequest, NotFound } from '../common/exceptions';
import type { Credentials } from '../databases/postgres/tables'; import type { Credentials } from '../databases/postgres/tables';
import { CredentialsRepository } from '../repositories/credentials.repository'; import { CredentialsRepository } from '../repositories/credentials.repository';
import { UsersRepository } from '../repositories/users.repository'; import { UsersRepository } from '../repositories/users.repository';
@ -39,7 +39,7 @@ export class LoginRequestsService {
const existingUser = await this.usersRepository.findOneByUsername(data.username); const existingUser = await this.usersRepository.findOneByUsername(data.username);
if (!existingUser) { if (!existingUser) {
throw BadRequest('User not found'); throw NotFound('User not found');
} }
const credential = await this.credentialsRepository.findPasswordCredentialsByUserId(existingUser.id); const credential = await this.credentialsRepository.findPasswordCredentialsByUserId(existingUser.id);
@ -54,10 +54,15 @@ 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 && totpCredentials.secret_data !== null && totpCredentials.secret_data !== ''); return await this.createUserSession(
existingUser.id,
req,
!!totpCredentials && totpCredentials.secret_data !== null && totpCredentials.secret_data !== '',
false,
);
} }
async createUserSession(existingUserId: string, req: HonoRequest, twoFactorAuthEnabled: boolean) { async createUserSession(existingUserId: string, req: HonoRequest, twoFactorAuthEnabled: boolean, twoFactorVerified = false) {
const requestIpAddress = req.header('X-Forwarded-For'); 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(
@ -66,7 +71,7 @@ export class LoginRequestsService {
requestIpCountry || 'unknown', requestIpCountry || 'unknown',
requestIpAddress || 'unknown', requestIpAddress || 'unknown',
twoFactorAuthEnabled, twoFactorAuthEnabled,
false, twoFactorVerified,
); );
} }

View file

@ -5,15 +5,36 @@ import type {Disposable} from 'tsyringe';
@injectable() @injectable()
export class RedisService implements Disposable { export class RedisService implements Disposable {
readonly client: Redis readonly client: Redis;
constructor() { constructor() {
this.client = new Redis(config.redis.url, { this.client = new Redis(config.redis.url, {
maxRetriesPerRequest: null, maxRetriesPerRequest: null,
}) });
} }
async dispose(): Promise<void> { async get(data: { prefix: string; key: string }): Promise<string | null> {
this.client.disconnect() return this.client.get(`${data.prefix}:${data.key}`);
} }
async set(data: { prefix: string; key: string; value: string }): Promise<void> {
await this.client.set(`${data.prefix}:${data.key}`, data.value);
}
async delete(data: { prefix: string; key: string }): Promise<void> {
await this.client.del(`${data.prefix}:${data.key}`);
}
async setWithExpiry(data: {
prefix: string;
key: string;
value: string;
expiry: number;
}): Promise<void> {
await this.client.set(`${data.prefix}:${data.key}`, data.value, 'EXAT', Math.floor(data.expiry));
}
async dispose(): Promise<void> {
this.client.disconnect();
}
} }

View file

@ -17,119 +17,119 @@ export type RedisSession = {
}; };
export type Session = { export type Session = {
id: string; id: string;
userId: string; userId: string;
expiresAt: Date; expiresAt: Date;
ipCountry: string; ipCountry: string;
ipAddress: string; ipAddress: string;
twoFactorAuthEnabled: boolean; twoFactorEnabled: boolean;
isTwoFactorAuthenticated: boolean; twoFactorVerified: boolean;
}; };
export type SessionValidationResult = { session: Session; user: Users } | { session: null; user: null } | { session: Session; user: undefined }; export type SessionValidationResult = { session: Session; user: Users } | { session: null; user: null } | { session: Session; user: undefined };
@injectable() @injectable()
export class SessionsService { export class SessionsService {
constructor( constructor(
private redisService = inject(RedisService), private redisService = inject(RedisService),
private usersRepository = inject(UsersRepository), private usersRepository = inject(UsersRepository),
) {} ) {}
generateSessionToken() { generateSessionToken() {
const bytes = new Uint8Array(20); const bytes = new Uint8Array(20);
crypto.getRandomValues(bytes); crypto.getRandomValues(bytes);
return encodeBase32LowerCaseNoPadding(bytes); return encodeBase32LowerCaseNoPadding(bytes);
} }
async createSession( async createSession(
token: string, token: string,
userId: string, userId: string,
ipCountry: string, ipCountry: string,
ipAddress: string, ipAddress: string,
twoFactorAuthEnabled: boolean, twoFactorAuthEnabled: boolean,
isTwoFactorAuthenticated: boolean, twoFactorVerified: boolean,
) { ) {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const session = { const session = {
id: sessionId, id: sessionId,
userId, userId,
expiresAt: cookieExpiresAt, expiresAt: cookieExpiresAt,
ipCountry, ipCountry,
ipAddress, ipAddress,
twoFactorAuthEnabled, twoFactorAuthEnabled,
isTwoFactorAuthenticated, twoFactorVerified,
}; };
await this.redisService.client.set( await this.redisService.client.set(
`session:${sessionId}`, `session:${sessionId}`,
JSON.stringify({ JSON.stringify({
id: session.id, id: session.id,
user_id: session.userId, user_id: session.userId,
expires_at: session.expiresAt, expires_at: session.expiresAt,
ip_country: session.ipCountry, ip_country: session.ipCountry,
ip_address: session.ipAddress, ip_address: session.ipAddress,
two_factor_auth_enabled: session.twoFactorAuthEnabled, two_factor_auth_enabled: session.twoFactorAuthEnabled,
is_two_factor_authenticated: session.isTwoFactorAuthenticated, is_two_factor_authenticated: session.twoFactorVerified,
}), }),
'EXAT', 'EXAT',
Math.floor(session.expiresAt.getTime() / 1000), Math.floor(session.expiresAt.getTime() / 1000),
); );
return session; return session;
} }
async validateSessionToken(token: string): Promise<SessionValidationResult> { async validateSessionToken(token: string): Promise<SessionValidationResult> {
// TODO: Why was this needed in the docs? https://lucia-next.pages.dev/sessions/basic-api/drizzle-orm // TODO: Why was this needed in the docs? https://lucia-next.pages.dev/sessions/basic-api/drizzle-orm
// const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); // const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const item = await this.redisService.client.get(`session:${token}`); const item = await this.redisService.client.get(`session:${token}`);
if (item === null) { if (item === null) {
return { return {
session: null, session: null,
user: null, user: null,
}; };
} }
const result: RedisSession = JSON.parse(item); const result: RedisSession = JSON.parse(item);
const session: Session = { const session: Session = {
id: result.id, id: result.id,
userId: result.user_id, userId: result.user_id,
expiresAt: new Date(result.expires_at * 1000), expiresAt: new Date(result.expires_at * 1000),
ipCountry: result.ip_country, ipCountry: result.ip_country,
ipAddress: result.ip_address, ipAddress: result.ip_address,
twoFactorAuthEnabled: result.two_factor_auth_enabled, twoFactorEnabled: result.two_factor_auth_enabled,
isTwoFactorAuthenticated: result.is_two_factor_authenticated, twoFactorVerified: result.is_two_factor_authenticated,
}; };
let user: Users | undefined = undefined; let user: Users | undefined = undefined;
if (session.userId) { if (session.userId) {
user = await this.usersRepository.findOneById(session.userId); user = await this.usersRepository.findOneById(session.userId);
} }
if (Date.now() >= session.expiresAt.getTime()) { if (Date.now() >= session.expiresAt.getTime()) {
await this.redisService.client.del(`session:${token}`); await this.redisService.client.del(`session:${token}`);
return { return {
session: null, session: null,
user: null, user: null,
}; };
} }
if (Date.now() >= session.expiresAt.getTime() - cookieExpiresMilliseconds) { if (Date.now() >= session.expiresAt.getTime() - cookieExpiresMilliseconds) {
session.expiresAt = new Date(Date.now() + halfCookieExpiresMilliseconds); session.expiresAt = new Date(Date.now() + halfCookieExpiresMilliseconds);
await this.redisService.client.set( await this.redisService.client.set(
`session:${token}`, `session:${token}`,
JSON.stringify({ JSON.stringify({
id: session.id, id: session.id,
user_id: session.userId, user_id: session.userId,
expires_at: Math.floor(session.expiresAt.getTime() / 1000), expires_at: Math.floor(session.expiresAt.getTime() / 1000),
ip_country: session.ipCountry, ip_country: session.ipCountry,
ip_address: session.ipAddress, ip_address: session.ipAddress,
two_factor_auth_enabled: session.twoFactorAuthEnabled, two_factor_enabled: session.twoFactorEnabled,
is_two_factor_authenticated: session.isTwoFactorAuthenticated, two_factor_verified: session.twoFactorVerified,
}), }),
'EXAT', 'EXAT',
Math.floor(session.expiresAt.getTime() / 1000), Math.floor(session.expiresAt.getTime() / 1000),
); );
} }
return { session, user }; return { session, user };
} }
async invalidateSession(sessionId: string) { async invalidateSession(sessionId: string) {
await this.redisService.client.del(`session:${sessionId}`); await this.redisService.client.del(`session:${sessionId}`);
} }
} }

View file

@ -1,19 +1,20 @@
import {eq} from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import {generateIdFromEntropySize, type Session, type User} from 'lucia'; import { generateIdFromEntropySize } from 'lucia';
import {createDate, TimeSpan} from 'oslo'; import { createDate, TimeSpan } from 'oslo';
import {password_reset_tokens} from './api/databases/postgres/tables'; import { password_reset_tokens, type Users } from './api/databases/postgres/tables';
import {db} from './api/packages/drizzle'; import { db } from './api/packages/drizzle';
import type { Session } from './api/services/sessions.service';
export async function createPasswordResetToken(userId: string): Promise<string> { export async function createPasswordResetToken(userId: string): Promise<string> {
// optionally invalidate all existing tokens // optionally invalidate all existing tokens
await db.delete(password_reset_tokens).where(eq(password_reset_tokens.user_id, userId)); await db.delete(password_reset_tokens).where(eq(password_reset_tokens.user_id, userId));
const tokenId = generateIdFromEntropySize(40); const tokenId = generateIdFromEntropySize(40);
await db.insert(password_reset_tokens).values({ await db.insert(password_reset_tokens).values({
id: tokenId, id: tokenId,
user_id: userId, user_id: userId,
expires_at: createDate(new TimeSpan(2, 'h')), expires_at: createDate(new TimeSpan(2, 'h')),
}); });
return tokenId; return tokenId;
} }
/** /**
@ -23,8 +24,8 @@ export async function createPasswordResetToken(userId: string): Promise<string>
* @param session - The session object. * @param session - The session object.
* @returns True if the user is not fully authenticated, otherwise false. * @returns True if the user is not fully authenticated, otherwise false.
*/ */
export function userNotFullyAuthenticated(user: User | null, session: Session | null) { export function userNotFullyAuthenticated(user: Users | null, session: Session | null) {
return user && session && session.isTwoFactorAuthEnabled && !session.isTwoFactorAuthenticated; return user && session && session.twoFactorEnabled && !session.twoFactorVerified;
} }
/** /**
@ -35,7 +36,7 @@ export function userNotFullyAuthenticated(user: User | null, session: Session |
* @returns {boolean} True if the user is not fully authenticated, otherwise false. * @returns {boolean} True if the user is not fully authenticated, otherwise false.
*/ */
export function userNotAuthenticated(user: User | null, session: Session | null) { export function userNotAuthenticated(user: User | null, session: Session | null) {
return !user || !session || userNotFullyAuthenticated(user, session); return !user || !session || userNotFullyAuthenticated(user, session);
} }
/** /**
@ -46,5 +47,5 @@ export function userNotAuthenticated(user: User | null, session: Session | null)
* @returns {boolean} True if the user is fully authenticated, otherwise false. * @returns {boolean} True if the user is fully authenticated, otherwise false.
*/ */
export function userFullyAuthenticated(user: User | null, session: Session | null) { export function userFullyAuthenticated(user: User | null, session: Session | null) {
return !userNotAuthenticated(user, session); return !userNotAuthenticated(user, session);
} }

View file

@ -0,0 +1,28 @@
import { loadFlash } from 'sveltekit-flash-message/server';
import type { LayoutServerLoad } from '../$types';
import { redirect } from 'sveltekit-flash-message/server';
import { notSignedInMessage } from '$lib/flashMessages';
export const load: LayoutServerLoad = loadFlash(async (event) => {
const { locals, url } = event;
const { user, session } = await locals.getAuthedUser();
console.log('User from protected route', user);
console.log('Session from protected route', session);
if (session === null) {
throw redirect(302, '/landing', notSignedInMessage, event);
}
if (session?.twoFactorEnabled && !session?.twoFactorVerified) {
throw redirect(302, '/login', notSignedInMessage, event);
}
return {
url: url.pathname,
user: {
cuid: user?.cuid,
firstName: user?.firstName,
lastName: user?.lastName,
username: user?.username,
},
};
});

View file

@ -0,0 +1,52 @@
import { fail } from '@sveltejs/kit';
import type { MetaTagsProps } from 'svelte-meta-tags';
import type { PageServerLoad } from '../$types';
import { notSignedInMessage } from '$lib/flashMessages';
import { redirect } from 'sveltekit-flash-message/server';
export const load: PageServerLoad = async (event) => {
const { locals, url } = event;
const image = {
url: `${new URL(url.pathname, url.origin).href}og?header=Bored Game&page=Home&content=Keep track of your games`,
width: 1200,
height: 630,
};
const metaTags: MetaTagsProps = Object.freeze({
title: 'Home',
description: 'Home page',
openGraph: {
type: 'website',
url: new URL(url.pathname, url.origin).href,
locale: 'en_US',
title: 'Home',
description: 'Bored Game, keep track of your games',
images: [image],
siteName: 'Bored Game',
},
twitter: {
handle: '@boredgame',
site: '@boredgame',
cardType: 'summary_large_image',
title: 'Home | Bored Game',
description: 'Bored Game, keep track of your games',
image: `${new URL(url.pathname, url.origin).href}og?header=Bored Game&page=Home&content=Keep track of your games`,
imageAlt: 'Home | Bored Game',
},
});
const { data: wishlistsData, error: wishlistsError } = await locals.api.wishlists.$get().then(locals.parseApiResponse);
const { data: collectionsData, error: collectionsError } = await locals.api.collections.$get().then(locals.parseApiResponse);
if (wishlistsError || collectionsError) {
return fail(500);
}
console.log('Wishlists', wishlistsData.wishlists);
console.log('Collections', collectionsData.collections);
return {
metaTagsChild: metaTags,
wishlists: wishlistsData.wishlists,
collections: collectionsData.collections,
};
};

View file

@ -1,22 +1,25 @@
<script lang="ts"> <script lang="ts">
const { data } = $props() import { page } from "$app/stores";
const { user, wishlists = [], collections = [] } = data import type { PageData } from './$types';
const welcomeName = $derived.by(() => { let { data }: { data: PageData } = $props();
let welcomeName = '' const { user, wishlists = [], collections = [] } = data;
if (data?.user?.firstName) {
welcomeName += data?.user?.firstName
}
if (data?.user?.lastName) {
welcomeName = welcomeName.length === 0 ? data?.user?.lastName : welcomeName
}
if (welcomeName.length === 0) { const welcomeName = $derived.by(() => {
return user?.username let welcomeName = "";
} if (data?.user?.firstName) {
welcomeName += data?.user?.firstName;
}
if (data?.user?.lastName) {
welcomeName = welcomeName.length === 0 ? data?.user?.lastName : welcomeName;
}
return welcomeName if (welcomeName.length === 0) {
}) return user?.username;
}
return welcomeName;
});
</script> </script>
<div class="container"> <div class="container">
@ -37,7 +40,10 @@ const welcomeName = $derived.by(() => {
{:else} {:else}
<h1>Welcome to Bored Game!</h1> <h1>Welcome to Bored Game!</h1>
<h2>Track the board games you own, the ones you want, and whether you play them enough.</h2> <h2>Track the board games you own, the ones you want, and whether you play them enough.</h2>
<p>Get started by joining the <a href="/waitlist">wait list</a> or <a href="/login">log in</a> if you already have an account.</p> <p>
Get started by joining the <a href="/waitlist">wait list</a> or <a href="/login">log in</a> if you already have an
account.
</p>
{/if} {/if}
</div> </div>

View file

@ -0,0 +1,39 @@
import { fail, redirect } from '@sveltejs/kit';
import type { MetaTagsProps } from 'svelte-meta-tags';
import type { PageServerLoad } from '../$types';
export const load: PageServerLoad = async (event) => {
const { locals, url } = event;
const image = {
url: `${new URL(url.pathname, url.origin).href}og?header=Bored Game&page=Home&content=Keep track of your games`,
width: 1200,
height: 630,
};
const metaTags: MetaTagsProps = Object.freeze({
title: 'Home',
description: 'Home page',
openGraph: {
type: 'website',
url: new URL(url.pathname, url.origin).href,
locale: 'en_US',
title: 'Home',
description: 'Bored Game, keep track of your games',
images: [image],
siteName: 'Bored Game',
},
twitter: {
handle: '@boredgame',
site: '@boredgame',
cardType: 'summary_large_image',
title: 'Home | Bored Game',
description: 'Bored Game, keep track of your games',
image: `${new URL(url.pathname, url.origin).href}og?header=Bored Game&page=Home&content=Keep track of your games`,
imageAlt: 'Home | Bored Game',
},
});
return {
metaTagsChild: metaTags,
};
};

View file

@ -0,0 +1,26 @@
<script lang="ts">
const { data } = $props();
</script>
<div class="container">
<h1>Welcome to Bored Game!</h1>
<h2>Track the board games you own, the ones you want, and whether you play them enough.</h2>
<p>
Get started by joining the <a href="/waitlist">wait list</a> or <a href="/login">log in</a> if you already have an account.
</p>
</div>
<style lang="postcss">
a {
text-decoration: underline;
}
.container {
display: grid;
place-content: center;
max-width: 900px;
gap: 0.25rem;
margin-left: auto;
margin-right: auto;
}
</style>

View file

@ -3,10 +3,15 @@ import type { LayoutServerLoad } from '../$types';
export const load: LayoutServerLoad = loadFlash(async (event) => { export const load: LayoutServerLoad = loadFlash(async (event) => {
const { url, locals } = event; const { url, locals } = event;
const authedUser = await locals.getAuthedUser(); const { user } = await locals.getAuthedUser();
return { return {
url: url.pathname, url: url.pathname,
authedUser, user: user ? {
cuid: user?.cuid,
firstName: user?.firstName,
lastName: user?.lastName,
username: user?.username,
} : null,
}; };
}); });

View file

@ -7,7 +7,7 @@ const { data, children } = $props();
</script> </script>
<div class="flex min-h-screen w-full flex-col"> <div class="flex min-h-screen w-full flex-col">
<Header user={data.authedUser} /> <Header user={data.user} />
<main <main
class="flex min-h-[calc(100vh-theme(spacing.16))] flex-1 flex-col gap-4 p-4 md:gap-8 md:p-10" class="flex min-h-[calc(100vh-theme(spacing.16))] flex-1 flex-col gap-4 p-4 md:gap-8 md:p-10"

View file

@ -1,63 +0,0 @@
import { fail } from '@sveltejs/kit';
import type { MetaTagsProps } from 'svelte-meta-tags';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => {
const { locals, url } = event;
const authedUser = await locals.getAuthedUser();
const image = {
url: `${new URL(url.pathname, url.origin).href}og?header=Bored Game&page=Home&content=Keep track of your games`,
width: 1200,
height: 630,
};
const metaTags: MetaTagsProps = Object.freeze({
title: 'Home',
description: 'Home page',
openGraph: {
type: 'website',
url: new URL(url.pathname, url.origin).href,
locale: 'en_US',
title: 'Home',
description: 'Bored Game, keep track of your games',
images: [image],
siteName: 'Bored Game',
},
twitter: {
handle: '@boredgame',
site: '@boredgame',
cardType: 'summary_large_image',
title: 'Home | Bored Game',
description: 'Bored Game, keep track of your games',
image: `${new URL(url.pathname, url.origin).href}og?header=Bored Game&page=Home&content=Keep track of your games`,
imageAlt: 'Home | Bored Game',
},
});
if (authedUser) {
const { data: wishlistsData, error: wishlistsError } = await locals.api.wishlists.$get().then(locals.parseApiResponse);
const { data: collectionsData, error: collectionsError } = await locals.api.collections.$get().then(locals.parseApiResponse);
if (wishlistsError || collectionsError) {
return fail(500, 'Failed to fetch wishlistsTable or collections');
}
console.log('Wishlists', wishlistsData.wishlists);
console.log('Collections', collectionsData.collections);
return {
metaTagsChild: metaTags,
user: {
firstName: authedUser?.firstName,
lastName: authedUser?.lastName,
username: authedUser?.username,
},
wishlists: wishlistsData.wishlists,
collections: collectionsData.collections,
};
}
console.log('Not Authed');
return { metaTagsChild: metaTags, user: null, wishlists: [], collections: [] };
};

View file

@ -1,9 +0,0 @@
import { dev } from '$app/environment';
// we don't need any JS on this page, though we'll load
// it in dev so that we get hot module replacement...
export const csr = dev;
// since there's no dynamic data here, we can prerender
// it so that it gets served as a static asset in prod
export const prerender = true;

View file

@ -1 +0,0 @@
export const prerender = true;

View file

@ -1 +0,0 @@
export const prerender = true;

View file

@ -1,8 +1,11 @@
import { redirect } from '@sveltejs/kit';
export async function load(event) { export async function load(event) {
const { url, locals } = event; const { url, locals } = event;
const { user, session } = await locals.getAuthedUser();
return { return {
url: url.pathname, url: url.pathname,
user: locals.user, user,
}; };
} }

View file

@ -9,9 +9,9 @@ import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
const { locals } = event; const { locals } = event;
const authedUser = await locals.getAuthedUser(); const { user } = await locals.getAuthedUser();
if (authedUser) { if (user) {
console.log('user already signed in'); console.log('user already signed in');
const message = { type: 'success', message: 'You are already signed in' } as const; const message = { type: 'success', message: 'You are already signed in' } as const;
throw redirect('/', message, event); throw redirect('/', message, event);
@ -28,9 +28,9 @@ export const actions: Actions = {
default: async (event) => { default: async (event) => {
const { locals } = event; const { locals } = event;
const authedUser = await locals.getAuthedUser(); const { user } = await locals.getAuthedUser();
if (authedUser) { if (user) {
const message = { type: 'success', message: 'You are already signed in' } as const; const message = { type: 'success', message: 'You are already signed in' } as const;
throw redirect('/', message, event); throw redirect('/', message, event);
} }
@ -38,8 +38,10 @@ export const actions: Actions = {
const form = await superValidate(event, zod(signinUsernameDto)); const form = await superValidate(event, zod(signinUsernameDto));
const { error } = await locals.api.login.$post({ json: form.data }).then(locals.parseApiResponse); const { error } = await locals.api.login.$post({ json: form.data }).then(locals.parseApiResponse);
console.log('Login error', error);
if (error) { if (error) {
return setError(form, 'username', error); console.log('Setting error');
return setError(form, 'username', 'An error occurred while logging in.');
} }
if (!form.valid) { if (!form.valid) {

View file

@ -6,149 +6,59 @@ import { setError, superValidate } from 'sveltekit-superforms/server';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
const signUpDefaults = { const signUpDefaults = {
firstName: '', firstName: '',
lastName: '', lastName: '',
email: '', email: '',
username: '', username: '',
password: '', password: '',
confirm_password: '', confirm_password: '',
terms: true, terms: true,
}; };
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
const { locals } = event; const { locals } = event;
const authedUser = await locals.getAuthedUser(); const { user } = await locals.getAuthedUser();
if (authedUser) { if (user) {
const message = { type: 'success', message: 'You are already signed in' } as const; const message = { type: 'success', message: 'You are already signed in' } as const;
throw redirect('/', message, event); throw redirect('/', message, event);
} }
// if (userFullyAuthenticated(user, session)) { return {
// const message = { type: 'success', message: 'You are already signed in' } as const; signupForm: await superValidate(zod(signupUsernameEmailDto), {
// throw redirect('/', message, event); defaults: signUpDefaults,
// } else if (userNotFullyAuthenticated(user, session)) { }),
// try { };
// await lucia.invalidateSession(locals.session!.id!);
// } catch (error) {
// console.log('Session already invalidated');
// }
// const sessionCookie = lucia.createBlankSessionCookie();
// cookies.set(sessionCookie.name, sessionCookie.value, {
// path: '.',
// ...sessionCookie.attributes,
// });
// }
return {
signupForm: await superValidate(zod(signupUsernameEmailDto), {
defaults: signUpDefaults,
}),
};
}; };
export const actions: Actions = { export const actions: Actions = {
default: async (event) => { default: async (event) => {
const { locals } = event; const { locals } = event;
const authedUser = await locals.getAuthedUser(); const { user } = await locals.getAuthedUser();
if (authedUser) { if (user) {
const message = { type: 'success', message: 'You are already signed in' } as const; const message = { type: 'success', message: 'You are already signed in' } as const;
throw redirect('/', message, event); throw redirect('/', message, event);
} }
const form = await superValidate(event, zod(signupUsernameEmailDto)); const form = await superValidate(event, zod(signupUsernameEmailDto));
const { error } = await locals.api.signup.$post({ json: form.data }).then(locals.parseApiResponse); const { error } = await locals.api.signup.$post({ json: form.data }).then(locals.parseApiResponse);
if (error) return setError(form, 'username', error); if (error) {
if (!form.valid) {
form.data.password = ''; form.data.password = '';
form.data.confirm_password = ''; return setError(form, 'username', 'Unable to log in.');
return fail(400, { }
form,
});
}
// let session; if (!form.valid) {
// let sessionCookie; form.data.password = '';
// // Adding user to the db form.data.confirm_password = '';
// console.log('Check if user already exists'); return fail(400, {
// form,
// const existing_user = await db.query.usersTable.findFirst({ });
// where: eq(usersTable.username, form.data.username), }
// });
//
// if (existing_user) {
// return setError(form, 'username', 'You cannot create an account with that username');
// }
//
// console.log('Creating user');
//
// const hashedPassword = await new Argon2id().hash(form.data.password);
//
// const user = await db
// .insert(usersTable)
// .values({
// username: form.data.username,
// hashed_password: hashedPassword,
// email: form.data.email,
// first_name: form.data.firstName ?? '',
// last_name: form.data.lastName ?? '',
// verified: false,
// receive_email: false,
// theme: 'system',
// })
// .returning();
// console.log('signup user', user);
//
// if (!user || user.length === 0) {
// return fail(400, {
// form,
// message: `Could not create your account. Please try again. If the problem persists, please contact support. Error ID: ${cuid2()}`,
// });
// }
//
// await add_user_to_role(user[0].id, 'user', true);
// await db.insert(collections).values({
// user_id: user[0].id,
// });
// await db.insert(wishlistsTable).values({
// user_id: user[0].id,
// });
//
// try {
// session = await lucia.createSession(user[0].id, {
// ip_country: event.locals.ip,
// ip_address: event.locals.country,
// twoFactorAuthEnabled: false,
// isTwoFactorAuthenticated: false,
// });
// sessionCookie = lucia.createSessionCookie(session.id);
// } catch (e: any) {
// if (e.message.toUpperCase() === `DUPLICATE_KEY_ID`) {
// // key already exists
// console.error('Lucia Error: ', e);
// }
// console.log(e);
// const message = {
// type: 'error',
// message: 'Unable to create your account. Please try again.',
// };
// form.data.password = '';
// form.data.confirm_password = '';
// error(500, message);
// }
//
// event.cookies.set(sessionCookie.name, sessionCookie.value, {
// path: '.',
// ...sessionCookie.attributes,
// });
redirect(302, '/'); redirect(302, '/');
// const message = { type: 'success', message: 'Signed Up!' } as const; },
// throw flashRedirect(message, event);
},
}; };

View file

@ -13,19 +13,16 @@ import type { PageServerLoad, RequestEvent } from './$types';
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
const { locals } = event; const { locals } = event;
const { session } = await locals.getAuthedUser();
const authedUser = await locals.getAuthedUser(); if (session === null) {
if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event); throw redirect(302, '/login', notSignedInMessage, event);
} }
if (!session?.twoFactorEnabled) {
const { data } = await locals.api.mfa.totp.$get().then(locals.parseApiResponse);
if (!data) {
throw redirect(302, '/login', notSignedInMessage, event); throw redirect(302, '/login', notSignedInMessage, event);
} }
const { totpCredential } = data; if (session.twoFactorVerified) {
if (!totpCredential) { const message = { type: 'success', message: 'You are already signed in' } as const;
throw redirect(302, '/login', notSignedInMessage, event); throw redirect('/', message, event);
} }
return { return {
@ -38,10 +35,14 @@ export const actions: Actions = {
validateTotp: async (event) => { validateTotp: async (event) => {
const { locals } = event; const { locals } = event;
const authedUser = await locals.getAuthedUser(); const { session } = await locals.getAuthedUser();
if (!authedUser) { if (session === null) {
throw redirect(302, '/login', notSignedInMessage, event); throw redirect(302, '/login', notSignedInMessage, event);
} }
if (!session.twoFactorEnabled || session.twoFactorVerified) {
const message = { type: 'success', message: 'You are already signed in' } as const;
throw redirect('/', message, event);
}
const { data: totpData } = await locals.api.mfa.totp.$get().then(locals.parseApiResponse); const { data: totpData } = await locals.api.mfa.totp.$get().then(locals.parseApiResponse);
if (!totpData) { if (!totpData) {
@ -64,16 +65,24 @@ export const actions: Actions = {
return setError(totpForm, 'code', totpVerifyError); return setError(totpForm, 'code', totpVerifyError);
} }
console.log('Successfully logged in'); totpForm.data.code = '';
return message(totpForm, { type: 'success', message: 'Successfully logged in!' }); const message = { type: 'success', message: 'Successfully logged in!' } as const;
redirect(302, '/', message, event);
}, },
validateRecoveryCode: async (event) => { validateRecoveryCode: async (event) => {
const { cookies, locals } = event; const { cookies, locals } = event;
const authedUser = await locals.getAuthedUser(); const { session } = await locals.getAuthedUser();
if (!authedUser) { if (session === null) {
throw redirect(302, '/login', notSignedInMessage, event); throw redirect(302, '/login', notSignedInMessage, event);
} }
if (!session?.twoFactorEnabled) {
throw redirect(302, '/login', notSignedInMessage, event);
}
if (session.twoFactorVerified) {
const message = { type: 'success', message: 'You are already signed in' } as const;
throw redirect('/', message, event);
}
const { dbUser, twoFactorDetails } = await validateUserData(event, locals); const { dbUser, twoFactorDetails } = await validateUserData(event, locals);
@ -157,7 +166,7 @@ async function validateUserData(event: RequestEvent, locals: App.Locals) {
throw fail(401); throw fail(401);
} }
const isTwoFactorAuthenticated = session?.isTwoFactorAuthenticated; const twoFactorVerified = session?.twoFactorVerified;
const twoFactorDetails = await db.query.twoFactorTable.findFirst({ const twoFactorDetails = await db.query.twoFactorTable.findFirst({
where: eq(twoFactorTable.userId, dbUser!.id!), where: eq(twoFactorTable.userId, dbUser!.id!),
}); });
@ -167,7 +176,7 @@ async function validateUserData(event: RequestEvent, locals: App.Locals) {
throw redirect(302, '/login', message, event); throw redirect(302, '/login', message, event);
} }
if (isTwoFactorAuthenticated && twoFactorDetails.enabled && twoFactorDetails.secret !== '') { if (twoFactorVerified && twoFactorDetails.enabled && twoFactorDetails.secret !== '') {
const message = { type: 'success', message: 'You are already signed in' } as const; const message = { type: 'success', message: 'You are already signed in' } as const;
throw redirect('/', message, event); throw redirect('/', message, event);
} }

View file

@ -1,9 +1,11 @@
import { loadFlash } from 'sveltekit-flash-message/server'; import { loadFlash } from 'sveltekit-flash-message/server';
import type { LayoutServerLoad } from './$types'; import type { LayoutServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
export const load: LayoutServerLoad = loadFlash(async (event) => { export const load: LayoutServerLoad = loadFlash(async (event) => {
const { locals, url } = event; const { locals, url } = event;
const user = await locals.getAuthedUser(); const { user } = await locals.getAuthedUser();
return { return {
url: url.pathname, url: url.pathname,
user, user,