From 0f2344c70fe0b835d3883643ee9b4110f0cd5f61 Mon Sep 17 00:00:00 2001 From: Bradley Shellnut Date: Sat, 23 Nov 2024 14:49:16 -0800 Subject: [PATCH] Refactoring the app layout, fixing checks for session and not user to fix totp. --- package.json | 12 +- pnpm-lock.yaml | 128 +++++----- src/app.d.ts | 5 +- src/hooks.server.ts | 6 +- src/lib/server/api/common/types/hono.ts | 1 - .../api/controllers/collection.controller.ts | 62 ++--- .../server/api/controllers/iam.controller.ts | 225 +++++++++--------- .../api/controllers/login.controller.ts | 62 ++--- .../server/api/controllers/mfa.controller.ts | 19 +- .../api/controllers/signup.controller.ts | 2 +- .../server/api/controllers/user.controller.ts | 49 ++-- .../api/controllers/wishlist.controller.ts | 40 ++-- src/lib/server/api/databases/postgres/seed.ts | 1 - .../api/databases/postgres/tables/index.ts | 1 - .../postgres/tables/sessions.table.ts | 28 --- .../api/middleware/require-auth.middleware.ts | 34 ++- .../api/services/loginrequest.service.ts | 15 +- src/lib/server/api/services/redis.service.ts | 39 ++- .../server/api/services/sessions.service.ts | 206 ++++++++-------- src/lib/server/auth-utils.ts | 37 +-- .../(app)/(protected)/+layout.server.ts | 28 +++ src/routes/(app)/(protected)/+page.server.ts | 52 ++++ .../(app)/{ => (protected)}/+page.svelte | 38 +-- .../{ => (protected)}/game/[id]/+error.svelte | 0 .../game/[id]/+page.server.ts | 0 .../{ => (protected)}/game/[id]/+page.svelte | 0 .../{ => (protected)}/search/+error.svelte | 0 .../{ => (protected)}/search/+page.server.ts | 0 .../{ => (protected)}/search/+page.svelte | 0 .../(app)/{ => (public)}/about/+page.svelte | 0 .../(app)/(public)/landing/+page.server.ts | 39 +++ .../(app)/(public)/landing/+page.svelte | 26 ++ .../(app)/{ => (public)}/privacy/+page.svelte | 0 .../(app)/{ => (public)}/terms/+page.svelte | 0 .../{ => (public)}/waitlist/+page.svelte | 0 src/routes/(app)/+layout.server.ts | 9 +- src/routes/(app)/+layout.svelte | 2 +- src/routes/(app)/+page.server.ts | 63 ----- src/routes/(app)/about/+page.ts | 9 - src/routes/(app)/privacy/+page.server.ts | 1 - src/routes/(app)/terms/+page.server.ts | 1 - src/routes/(auth)/+layout.server.ts | 5 +- src/routes/(auth)/login/+page.server.ts | 12 +- src/routes/(auth)/signup/+page.server.ts | 168 +++---------- src/routes/(auth)/totp/+page.server.ts | 43 ++-- src/routes/+layout.server.ts | 4 +- 46 files changed, 748 insertions(+), 724 deletions(-) delete mode 100644 src/lib/server/api/databases/postgres/tables/sessions.table.ts create mode 100644 src/routes/(app)/(protected)/+layout.server.ts create mode 100644 src/routes/(app)/(protected)/+page.server.ts rename src/routes/(app)/{ => (protected)}/+page.svelte (53%) rename src/routes/(app)/{ => (protected)}/game/[id]/+error.svelte (100%) rename src/routes/(app)/{ => (protected)}/game/[id]/+page.server.ts (100%) rename src/routes/(app)/{ => (protected)}/game/[id]/+page.svelte (100%) rename src/routes/(app)/{ => (protected)}/search/+error.svelte (100%) rename src/routes/(app)/{ => (protected)}/search/+page.server.ts (100%) rename src/routes/(app)/{ => (protected)}/search/+page.svelte (100%) rename src/routes/(app)/{ => (public)}/about/+page.svelte (100%) create mode 100644 src/routes/(app)/(public)/landing/+page.server.ts create mode 100644 src/routes/(app)/(public)/landing/+page.svelte rename src/routes/(app)/{ => (public)}/privacy/+page.svelte (100%) rename src/routes/(app)/{ => (public)}/terms/+page.svelte (100%) rename src/routes/(app)/{ => (public)}/waitlist/+page.svelte (100%) delete mode 100644 src/routes/(app)/+page.server.ts delete mode 100644 src/routes/(app)/about/+page.ts delete mode 100644 src/routes/(app)/privacy/+page.server.ts delete mode 100644 src/routes/(app)/terms/+page.server.ts diff --git a/package.json b/package.json index 769c313..052dada 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "postcss-load-config": "^5.1.0", "postcss-preset-env": "^9.6.0", "prettier": "^3.3.3", - "prettier-plugin-svelte": "^3.2.8", + "prettier-plugin-svelte": "^3.3.0", "svelte": "5.0.0-next.175", "svelte-check": "^3.8.6", "svelte-headless-table": "^0.18.3", @@ -82,7 +82,7 @@ "@iconify-icons/line-md": "^1.2.30", "@iconify-icons/mdi": "^1.2.48", "@inlang/paraglide-sveltekit": "^0.11.1", - "@internationalized/date": "^3.5.6", + "@internationalized/date": "^3.6.0", "@lucia-auth/adapter-drizzle": "^1.1.0", "@lukeed/uuid": "^2.0.1", "@needle-di/core": "^0.8.4", @@ -96,22 +96,22 @@ "@oslojs/otp": "^1.0.0", "@oslojs/webauthn": "^1.0.0", "@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-vercel": "^5.4.7", "@types/feather-icons": "^4.29.4", "boardgamegeekclient": "^1.9.1", - "bullmq": "^5.27.0", + "bullmq": "^5.28.1", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", - "cookie": "^1.0.1", + "cookie": "^1.0.2", "dotenv": "^16.4.5", "dotenv-expand": "^11.0.7", "drizzle-orm": "^0.36.3", "drizzle-zod": "^0.5.1", "feather-icons": "^4.29.2", "handlebars": "^4.7.8", - "hono": "^4.6.10", + "hono": "^4.6.11", "hono-pino": "^0.3.0", "hono-rate-limiter": "^0.4.0", "hono-zod-openapi": "^0.4.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b082905..ab7e2ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,13 +13,13 @@ importers: version: 5.1.0 '@hono/swagger-ui': specifier: ^0.4.1 - version: 0.4.1(hono@4.6.10) + version: 0.4.1(hono@4.6.11) '@hono/zod-openapi': 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': 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': specifier: ^1.2.30 version: 1.2.30 @@ -30,8 +30,8 @@ importers: 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))) '@internationalized/date': - specifier: ^3.5.6 - version: 3.5.6 + specifier: ^3.6.0 + version: 3.6.0 '@lucia-auth/adapter-drizzle': 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) @@ -72,8 +72,8 @@ importers: specifier: ^2.2.2 version: 2.2.2 '@scalar/hono-api-reference': - specifier: ^0.5.159 - version: 0.5.159(hono@4.6.10) + specifier: ^0.5.160 + version: 0.5.160(hono@4.6.11) '@sveltejs/adapter-node': 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))) @@ -87,8 +87,8 @@ importers: specifier: ^1.9.1 version: 1.9.1 bullmq: - specifier: ^5.27.0 - version: 5.27.0 + specifier: ^5.28.1 + version: 5.28.1 class-variance-authority: specifier: ^0.7.0 version: 0.7.0 @@ -96,8 +96,8 @@ importers: specifier: ^2.1.1 version: 2.1.1 cookie: - specifier: ^1.0.1 - version: 1.0.1 + specifier: ^1.0.2 + version: 1.0.2 dotenv: specifier: ^16.4.5 version: 16.4.5 @@ -117,17 +117,17 @@ importers: specifier: ^4.7.8 version: 4.7.8 hono: - specifier: ^4.6.10 - version: 4.6.10 + specifier: ^4.6.11 + version: 4.6.11 hono-pino: 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: specifier: ^0.4.0 - version: 0.4.0(hono@4.6.10) + version: 0.4.0(hono@4.6.11) hono-zod-openapi: 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: specifier: ^2.5.2 version: 2.5.2 @@ -178,7 +178,7 @@ importers: version: 0.2.2 stoker: 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: specifier: ^1.0.0 version: 1.0.0 @@ -292,8 +292,8 @@ importers: specifier: ^3.3.3 version: 3.3.3 prettier-plugin-svelte: - specifier: ^3.2.8 - version: 3.2.8(prettier@3.3.3)(svelte@5.0.0-next.175) + specifier: ^3.3.0 + version: 3.3.0(prettier@3.3.3)(svelte@5.0.0-next.175) svelte: specifier: 5.0.0-next.175 version: 5.0.0-next.175 @@ -1665,8 +1665,8 @@ packages: '@inlang/translatable@1.3.1': resolution: {integrity: sha512-VAtle21vRpIrB+axtHFrFB0d1HtDaaNj+lV77eZQTJyOWbTFYTVIQJ8WAbyw9eu4F6h6QC2FutLyxjMomxfpcQ==} - '@internationalized/date@3.5.6': - resolution: {integrity: sha512-jLxQjefH9VI5P9UQuqB6qNKnvFt1Ky1TPIzHGsIlCi7sZZoMR8SdYbBGRvM0y+Jtb+ez4ieBzmiAUcpmPYpyOw==} + '@internationalized/date@3.6.0': + resolution: {integrity: sha512-+z6ti+CcJnRlLHok/emGEsWQhe7kfSmEW+/6qCzvKY67YPh7YOBfvc7+/+NXq+zJlbArg30tYpqLjNgcAYv2YQ==} '@ioredis/commands@1.2.0': resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} @@ -2344,8 +2344,8 @@ packages: cpu: [x64] os: [win32] - '@scalar/hono-api-reference@0.5.159': - resolution: {integrity: sha512-nUKaN0CKvytbXPj9b6taF/efKKRqEUwhVxlfLVjrJXN0eHNHDWxG9e/5Tyw1o2MXJo1cQpGZ4qTh48k/8u6ZjA==} + '@scalar/hono-api-reference@0.5.160': + resolution: {integrity: sha512-3NXXh4B+5sa9QEchtQNKH0me4pEB8soFfCSqOxsuB0d8agokDO574cP9Qq4l6vwSh6FMiqSdaGTv5459g8hTCg==} engines: {node: '>=18'} peerDependencies: hono: ^4.0.0 @@ -2354,8 +2354,8 @@ packages: resolution: {integrity: sha512-6geH9ehvQ/sG/xUyy3e0lyOw3BaY5s6nn22wHjEJhcobdmWyFER0O6m7AU0ZN4QTjle/gYvFJOjj552l/rsNSw==} engines: {node: '>=18'} - '@scalar/types@0.0.19': - resolution: {integrity: sha512-wOxtXd35BS0DaVhBopQUB8c8hfLQ+/PKEr99GbOZW+4DWCrEB8JfWJgvpJyxHU6by7LHNVY4fvpFQR7Ezh1IIw==} + '@scalar/types@0.0.20': + resolution: {integrity: sha512-Sx7tqiuV9ZNp2XpIXKD/srzTjjb4I6aof0Y7+U5MgEuKPwrRnYS/BaocdNgWLnpCZisBIg5qp4YSqozxibabVg==} engines: {node: '>=18'} '@sideway/address@4.1.5': @@ -2801,8 +2801,8 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - bullmq@5.27.0: - resolution: {integrity: sha512-DZWrjDLkecZZ1/43h/SkG6CxU8nO/Lq/0svVoQdw33ksUCGfccgjbvCa/cuxHP/OvhxlTAA0cO3dBOoaT7sRFQ==} + bullmq@5.28.1: + resolution: {integrity: sha512-iSoqziPLKH//mmoc4Aj3/opTmk1PgFdITwUrx/wDqrTxfBRjnTGInsu129LCEY6d+SmhZWnA9PYU6ciX+NT64A==} bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} @@ -2956,8 +2956,8 @@ packages: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} - cookie@1.0.1: - resolution: {integrity: sha512-Xd8lFX4LM9QEEwxQpF9J9NTUh8pmdJO0cyRJhFiDoLTk2eH8FXlRv2IFGYVadZpqI3j8fhNrSdKCeYPxiAhLXw==} + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} engines: {node: '>=18'} core-js@3.38.1: @@ -3659,8 +3659,8 @@ packages: hono: ^4.6.3 zod: ^3.21.4 - hono@4.6.10: - resolution: {integrity: sha512-IXXNfRAZEahFnWBhUUlqKEGF9upeE6hZoRZszvNkyAz/CYp+iVbxm3viMvStlagRJohjlBRGOQ7f4jfcV0XMGg==} + hono@4.6.11: + resolution: {integrity: sha512-f0LwJQFKdUUrCUAVowxSvNCjyzI7ZLt8XWYU/EApyeq5FfOvHFarBaE5rjU9HTNFk4RI0FkdB2edb3p/7xZjzQ==} engines: {node: '>=16.9.0'} hookable@5.5.3: @@ -4648,8 +4648,8 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-plugin-svelte@3.2.8: - resolution: {integrity: sha512-PAHmmU5cGZdnhW4mWhmvxuG2PVbbHIxUuPOdUKvfE+d4Qt2d29iU5VWrPdsaW5YqVEE0nqhlvN4eoKmVMpIF3Q==} + prettier-plugin-svelte@3.3.0: + resolution: {integrity: sha512-iNoYiQUx4zwqbQDW/bk0WR75w+QiY4fHJQpGQ5v8Yr7X5m7YoSvs2buUnhoYFXNAL32ULVmrjPSc0vVOHJsO0Q==} peerDependencies: prettier: ^3.0.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 optional: true - '@hono/swagger-ui@0.4.1(hono@4.6.10)': + '@hono/swagger-ui@0.4.1(hono@4.6.11)': 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: '@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: 4.6.10 + '@hono/zod-validator': 0.2.2(hono@4.6.11)(zod@3.23.8) + hono: 4.6.11 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: - hono: 4.6.10 + hono: 4.6.11 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: - hono: 4.6.10 + hono: 4.6.11 zod: 3.23.8 '@humanwhocodes/config-array@0.13.0': @@ -6571,7 +6571,7 @@ snapshots: dependencies: '@inlang/language-tag': 1.5.1 - '@internationalized/date@3.5.6': + '@internationalized/date@3.6.0': dependencies: '@swc/helpers': 0.5.13 @@ -6665,7 +6665,7 @@ snapshots: dependencies: '@floating-ui/core': 1.6.8 '@floating-ui/dom': 1.6.11 - '@internationalized/date': 3.5.6 + '@internationalized/date': 3.6.0 dequal: 2.0.3 focus-trap: 7.6.0 nanoid: 5.0.7 @@ -6675,7 +6675,7 @@ snapshots: dependencies: '@floating-ui/core': 1.6.8 '@floating-ui/dom': 1.6.11 - '@internationalized/date': 3.5.6 + '@internationalized/date': 3.6.0 dequal: 2.0.3 focus-trap: 7.6.0 nanoid: 5.0.7 @@ -7226,14 +7226,14 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.24.0': 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: - '@scalar/types': 0.0.19 - hono: 4.6.10 + '@scalar/types': 0.0.20 + hono: 4.6.11 '@scalar/openapi-types@0.1.5': {} - '@scalar/types@0.0.19': + '@scalar/types@0.0.20': dependencies: '@scalar/openapi-types': 0.1.5 '@unhead/schema': 1.11.11 @@ -7701,7 +7701,7 @@ snapshots: bits-ui@0.21.16(svelte@5.0.0-next.175): dependencies: - '@internationalized/date': 3.5.6 + '@internationalized/date': 3.6.0 '@melt-ui/svelte': 0.76.2(svelte@5.0.0-next.175) nanoid: 5.0.7 svelte: 5.0.0-next.175 @@ -7766,7 +7766,7 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 - bullmq@5.27.0: + bullmq@5.28.1: dependencies: cron-parser: 4.9.0 ioredis: 5.4.1 @@ -7921,7 +7921,7 @@ snapshots: cookie@0.6.0: {} - cookie@1.0.1: {} + cookie@1.0.2: {} core-js@3.38.1: {} @@ -8636,24 +8636,24 @@ snapshots: 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: defu: 6.1.4 - hono: 4.6.10 + hono: 4.6.11 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: - 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: - '@hono/zod-validator': 0.4.1(hono@4.6.10)(zod@3.23.8) - hono: 4.6.10 + '@hono/zod-validator': 0.4.1(hono@4.6.11)(zod@3.23.8) + hono: 4.6.11 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: {} @@ -9634,7 +9634,7 @@ snapshots: 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: prettier: 3.3.3 svelte: 5.0.0-next.175 @@ -9962,13 +9962,13 @@ snapshots: 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: '@asteasolutions/zod-to-openapi': 7.1.2(zod@3.23.8) - hono: 4.6.10 + hono: 4.6.11 openapi3-ts: 4.4.0 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: dependencies: diff --git a/src/app.d.ts b/src/app.d.ts index 078edae..f38c290 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,5 +1,6 @@ import type { ApiClient } from '$lib/server/api'; 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'; // See https://svelte.dev/docs/kit/types#app.d.ts @@ -16,8 +17,8 @@ declare global { interface Locals { api: ApiClient['api']; parseApiResponse: typeof parseApiResponse; - getAuthedUser: () => Promise | null>; - getAuthedUserOrThrow: () => Promise>; + getAuthedUser: () => Promise> | null>; + getAuthedUserOrThrow: () => Promise>>; } namespace Superforms { type Message = { diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 4db49e0..c3be7fa 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -21,12 +21,12 @@ const apiClient: Handle = async ({ event, resolve }) => { /* ----------------------------- Auth functions ----------------------------- */ async function getAuthedUser() { - const { data } = await api.user.$get().then(parseApiResponse); - return data?.user; + const { data } = await api.me.$get().then(parseApiResponse); + return { user: data?.user, session: data?.session }; } async function getAuthedUserOrThrow() { - const { data } = await api.user.$get().then(parseApiResponse); + const { data } = await api.me.$get().then(parseApiResponse); if (!data || !data.user) { throw redirect(StatusCodes.TEMPORARY_REDIRECT, "/"); } diff --git a/src/lib/server/api/common/types/hono.ts b/src/lib/server/api/common/types/hono.ts index 9c79e3d..92f20a9 100644 --- a/src/lib/server/api/common/types/hono.ts +++ b/src/lib/server/api/common/types/hono.ts @@ -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 { Hono } from 'hono'; import type { PinoLogger } from 'hono-pino'; diff --git a/src/lib/server/api/controllers/collection.controller.ts b/src/lib/server/api/controllers/collection.controller.ts index 58bf813..9cb0282 100644 --- a/src/lib/server/api/controllers/collection.controller.ts +++ b/src/lib/server/api/controllers/collection.controller.ts @@ -1,39 +1,39 @@ -import {StatusCodes} from '$lib/constants/status-codes'; -import {Controller} from '$lib/server/api/common/types/controller'; -import {allCollections, getCollectionByCUID, numberOfCollections} from '$lib/server/api/controllers/collection.routes'; -import {CollectionsService} from '$lib/server/api/services/collections.service'; -import {openApi} from 'hono-zod-openapi'; +import { StatusCodes } from '$lib/constants/status-codes'; +import { Controller } from '$lib/server/api/common/types/controller'; +import { allCollections, getCollectionByCUID, numberOfCollections } from '$lib/server/api/controllers/collection.routes'; +import { CollectionsService } from '$lib/server/api/services/collections.service'; +import { openApi } from 'hono-zod-openapi'; import { injectable, inject } from '@needle-di/core'; -import {requireAuth} from '../middleware/require-auth.middleware'; +import { requireFullAuth } from '../middleware/require-auth.middleware'; @injectable() export class CollectionController extends Controller { - constructor(private collectionsService = inject(CollectionsService)) { - super(); - } + constructor(private collectionsService = inject(CollectionsService)) { + super(); + } - routes() { - return this.controller - .get('/', requireAuth, openApi(allCollections), async (c) => { - const user = c.var.user; - const collections = await this.collectionsService.findAllByUserId(user.id); - console.log('collections service', collections); - return c.json({ collections }, StatusCodes.OK); - }) - .get('/count', requireAuth, openApi(numberOfCollections), async (c) => { - const user = c.var.user; - const collections = await this.collectionsService.findAllByUserIdWithDetails(user.id); - return c.json({ count: collections?.length || 0 }, StatusCodes.OK); - }) - .get('/:cuid', requireAuth, openApi(getCollectionByCUID), async (c) => { - const cuid = c.req.param('cuid'); - const collection = await this.collectionsService.findOneByCuid(cuid); + routes() { + return this.controller + .get('/', requireFullAuth, openApi(allCollections), async (c) => { + const user = c.var.user; + const collections = await this.collectionsService.findAllByUserId(user.id); + console.log('collections service', collections); + return c.json({ collections }, StatusCodes.OK); + }) + .get('/count', requireFullAuth, openApi(numberOfCollections), async (c) => { + const user = c.var.user; + const collections = await this.collectionsService.findAllByUserIdWithDetails(user.id); + return c.json({ count: collections?.length || 0 }, StatusCodes.OK); + }) + .get('/:cuid', requireFullAuth, openApi(getCollectionByCUID), async (c) => { + const cuid = c.req.param('cuid'); + const collection = await this.collectionsService.findOneByCuid(cuid); - if (!collection) { - return c.json('Collection not found', StatusCodes.NOT_FOUND); - } + if (!collection) { + return c.json('Collection not found', StatusCodes.NOT_FOUND); + } - return c.json({ collection }, StatusCodes.OK); - }); - } + return c.json({ collection }, StatusCodes.OK); + }); + } } diff --git a/src/lib/server/api/controllers/iam.controller.ts b/src/lib/server/api/controllers/iam.controller.ts index 30dbe12..daa62ea 100644 --- a/src/lib/server/api/controllers/iam.controller.ts +++ b/src/lib/server/api/controllers/iam.controller.ts @@ -1,116 +1,119 @@ -import {StatusCodes} from '$lib/constants/status-codes'; -import {Controller} from '$lib/server/api/common/types/controller'; -import {createBlankSessionTokenCookie, setSessionCookie} from '$lib/server/api/common/utils/cookies'; -import {changePasswordDto} from '$lib/server/api/dtos/change-password.dto'; -import {updateEmailDto} from '$lib/server/api/dtos/update-email.dto'; -import {updateProfileDto} from '$lib/server/api/dtos/update-profile.dto'; -import {verifyPasswordDto} from '$lib/server/api/dtos/verify-password.dto'; -import {limiter} from '$lib/server/api/middleware/rate-limiter.middleware'; -import {IamService} from '$lib/server/api/services/iam.service'; -import {LoginRequestsService} from '$lib/server/api/services/loginrequest.service'; -import {SessionsService} from '$lib/server/api/services/sessions.service'; -import {zValidator} from '@hono/zod-validator'; -import {openApi} from 'hono-zod-openapi'; -import { injectable, inject } from "@needle-di/core"; -import {requireAuth} from '../middleware/require-auth.middleware'; -import {iam, logout, updateEmail, updatePassword, updateProfile, verifyPassword} from './iam.routes'; +import { StatusCodes } from '$lib/constants/status-codes'; +import { Controller } from '$lib/server/api/common/types/controller'; +import { createBlankSessionTokenCookie, setSessionCookie } from '$lib/server/api/common/utils/cookies'; +import { changePasswordDto } from '$lib/server/api/dtos/change-password.dto'; +import { updateEmailDto } from '$lib/server/api/dtos/update-email.dto'; +import { updateProfileDto } from '$lib/server/api/dtos/update-profile.dto'; +import { verifyPasswordDto } from '$lib/server/api/dtos/verify-password.dto'; +import { limiter } from '$lib/server/api/middleware/rate-limiter.middleware'; +import { IamService } from '$lib/server/api/services/iam.service'; +import { LoginRequestsService } from '$lib/server/api/services/loginrequest.service'; +import { SessionsService } from '$lib/server/api/services/sessions.service'; +import { zValidator } from '@hono/zod-validator'; +import { openApi } from 'hono-zod-openapi'; +import { injectable, inject } from '@needle-di/core'; +import { requireFullAuth, requireTempAuth } from '../middleware/require-auth.middleware'; +import { iam, logout, updateEmail, updatePassword, updateProfile, verifyPassword } from './iam.routes'; +import { UsersRepository } from '../repositories/users.repository'; @injectable() export class IamController extends Controller { - constructor( - private iamService = inject(IamService), - private loginRequestService = inject(LoginRequestsService), - private sessionsService = inject(SessionsService), - ) { - super(); - } + constructor( + private iamService = inject(IamService), + private loginRequestService = inject(LoginRequestsService), + private sessionsService = inject(SessionsService), + private usersRepository = inject(UsersRepository), + ) { + super(); + } - routes() { - return this.controller - .get('/', requireAuth, openApi(iam), async (c) => { - const user = c.var.user; - return c.json({ user }); - }) - .put( - '/update/profile', - requireAuth, - openApi(updateProfile), - zValidator('json', updateProfileDto), - limiter({ limit: 30, minutes: 60 }), - async (c) => { - const user = c.var.user; - const { firstName, lastName, username } = c.req.valid('json'); - const updatedUser = await this.iamService.updateProfile(user.id, { firstName, lastName, username }); - if (!updatedUser) { - return c.json('Username already in use', StatusCodes.UNPROCESSABLE_ENTITY); - } - return c.json({ user: updatedUser }, StatusCodes.OK); - }, - ) - .post( - '/verify/password', - requireAuth, - zValidator('json', verifyPasswordDto), - openApi(verifyPassword), - limiter({ limit: 10, minutes: 60 }), - async (c) => { - const user = c.var.user; - const { password } = c.req.valid('json'); - const passwordVerified = await this.iamService.verifyPassword(user.id, { password }); - if (!passwordVerified) { - console.log('Incorrect password'); - return c.json('Incorrect password', StatusCodes.FORBIDDEN); - } - return c.json({}, StatusCodes.OK); - }, - ) - .put( - '/update/password', - requireAuth, - openApi(updatePassword), - zValidator('json', changePasswordDto), - limiter({ limit: 10, minutes: 60 }), - async (c) => { - const user = c.var.user; - const { password, confirm_password } = c.req.valid('json'); - if (password !== confirm_password) { - return c.json('Passwords do not match', StatusCodes.UNPROCESSABLE_ENTITY); - } - try { - await this.iamService.updatePassword(user.id, { password, confirm_password }); - await this.sessionsService.invalidateSession(user.id); - await this.loginRequestService.createUserSession(user.id, c.req, false); - const sessionCookie = createBlankSessionTokenCookie(); - setSessionCookie(c, sessionCookie); - return c.json({ status: 'success' }); - } catch (error) { - console.error('Error updating password', error); - return c.json('Error updating password', StatusCodes.INTERNAL_SERVER_ERROR); - } - }, - ) - .post( - '/update/email', - requireAuth, - openApi(updateEmail), - zValidator('json', updateEmailDto), - limiter({ limit: 10, minutes: 60 }), - async (c) => { - const user = c.var.user; - const { email } = c.req.valid('json'); - const updatedUser = await this.iamService.updateEmail(user.id, { email }); - if (!updatedUser) { - return c.json('Cannot change email address', StatusCodes.FORBIDDEN); - } - return c.json({ user: updatedUser }, StatusCodes.OK); - }, - ) - .post('/logout', requireAuth, openApi(logout), async (c) => { - const sessionId = c.var.session.id; - await this.iamService.logout(sessionId); - const sessionCookie = createBlankSessionTokenCookie(); - setSessionCookie(c, sessionCookie); - return c.json({ status: 'success' }); - }); - } + routes() { + return this.controller + .get('/', openApi(iam), async (c) => { + const session = c.var.session; + const user = session ? await this.usersRepository.findOneByIdOrThrow(session.userId) : null; + return c.json({ user, session }); + }) + .put( + '/update/profile', + requireFullAuth, + openApi(updateProfile), + zValidator('json', updateProfileDto), + limiter({ limit: 30, minutes: 60 }), + async (c) => { + const user = c.var.user; + const { firstName, lastName, username } = c.req.valid('json'); + const updatedUser = await this.iamService.updateProfile(user.id, { firstName, lastName, username }); + if (!updatedUser) { + return c.json('Username already in use', StatusCodes.UNPROCESSABLE_ENTITY); + } + return c.json({ user: updatedUser }, StatusCodes.OK); + }, + ) + .post( + '/verify/password', + requireFullAuth, + zValidator('json', verifyPasswordDto), + openApi(verifyPassword), + limiter({ limit: 10, minutes: 60 }), + async (c) => { + const user = c.var.user; + const { password } = c.req.valid('json'); + const passwordVerified = await this.iamService.verifyPassword(user.id, { password }); + if (!passwordVerified) { + console.log('Incorrect password'); + return c.json('Incorrect password', StatusCodes.FORBIDDEN); + } + return c.json({}, StatusCodes.OK); + }, + ) + .put( + '/update/password', + requireFullAuth, + openApi(updatePassword), + zValidator('json', changePasswordDto), + limiter({ limit: 10, minutes: 60 }), + async (c) => { + const user = c.var.user; + const { password, confirm_password } = c.req.valid('json'); + if (password !== confirm_password) { + return c.json('Passwords do not match', StatusCodes.UNPROCESSABLE_ENTITY); + } + try { + await this.iamService.updatePassword(user.id, { password, confirm_password }); + await this.sessionsService.invalidateSession(user.id); + await this.loginRequestService.createUserSession(user.id, c.req, false, false); + const sessionCookie = createBlankSessionTokenCookie(); + setSessionCookie(c, sessionCookie); + return c.json({ status: 'success' }); + } catch (error) { + console.error('Error updating password', error); + return c.json('Error updating password', StatusCodes.INTERNAL_SERVER_ERROR); + } + }, + ) + .post( + '/update/email', + requireFullAuth, + openApi(updateEmail), + zValidator('json', updateEmailDto), + limiter({ limit: 10, minutes: 60 }), + async (c) => { + const user = c.var.user; + const { email } = c.req.valid('json'); + const updatedUser = await this.iamService.updateEmail(user.id, { email }); + if (!updatedUser) { + return c.json('Cannot change email address', StatusCodes.FORBIDDEN); + } + return c.json({ user: updatedUser }, StatusCodes.OK); + }, + ) + .post('/logout', requireFullAuth, openApi(logout), async (c) => { + const sessionId = c.var.session.id; + await this.iamService.logout(sessionId); + const sessionCookie = createBlankSessionTokenCookie(); + setSessionCookie(c, sessionCookie); + return c.json({ status: 'success' }); + }); + } } diff --git a/src/lib/server/api/controllers/login.controller.ts b/src/lib/server/api/controllers/login.controller.ts index a3de23e..7fa627a 100644 --- a/src/lib/server/api/controllers/login.controller.ts +++ b/src/lib/server/api/controllers/login.controller.ts @@ -1,37 +1,37 @@ -import {Controller} from '$lib/server/api/common/types/controller'; -import {cookieExpiresAt, createSessionTokenCookie, setSessionCookie} from '$lib/server/api/common/utils/cookies'; -import {signinUsernameDto} from '$lib/server/api/dtos/signin-username.dto'; -import {SessionsService} from '$lib/server/api/services/sessions.service'; -import {zValidator} from '@hono/zod-validator'; -import {openApi} from 'hono-zod-openapi'; +import { Controller } from '$lib/server/api/common/types/controller'; +import { cookieExpiresAt, createSessionTokenCookie, setSessionCookie } from '$lib/server/api/common/utils/cookies'; +import { signinUsernameDto } from '$lib/server/api/dtos/signin-username.dto'; +import { SessionsService } from '$lib/server/api/services/sessions.service'; +import { zValidator } from '@hono/zod-validator'; +import { openApi } from 'hono-zod-openapi'; import { inject, injectable } from '@needle-di/core'; -import {limiter} from '../middleware/rate-limiter.middleware'; -import {LoginRequestsService} from '../services/loginrequest.service'; -import {signinUsername} from './login.routes'; +import { limiter } from '../middleware/rate-limiter.middleware'; +import { LoginRequestsService } from '../services/loginrequest.service'; +import { signinUsername } from './login.routes'; @injectable() export class LoginController extends Controller { - constructor( - private loginRequestsService = inject(LoginRequestsService), - private sessionsService = inject(SessionsService), - ) { - super(); - } + constructor( + private loginRequestsService = inject(LoginRequestsService), + private sessionsService = inject(SessionsService), + ) { + super(); + } - routes() { - return this.controller.post( - '/', - openApi(signinUsername), - zValidator('json', signinUsernameDto), - limiter({ limit: 10, minutes: 60 }), - async (c) => { - const { username, password } = c.req.valid('json'); - const session = await this.loginRequestsService.verify({ username, password }, c.req); - const sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt); - console.log('set cookie', sessionCookie); - setSessionCookie(c, sessionCookie); - return c.json({ message: 'ok' }); - }, - ); - } + routes() { + return this.controller.post( + '/', + openApi(signinUsername), + zValidator('json', signinUsernameDto), + limiter({ limit: 10, minutes: 60 }), + async (c) => { + const { username, password } = c.req.valid('json'); + const session = await this.loginRequestsService.verify({ username, password }, c.req); + const sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt); + console.log('set cookie', sessionCookie); + setSessionCookie(c, sessionCookie); + return c.json({ message: 'ok' }); + }, + ); + } } diff --git a/src/lib/server/api/controllers/mfa.controller.ts b/src/lib/server/api/controllers/mfa.controller.ts index 23af75b..0ed71ef 100644 --- a/src/lib/server/api/controllers/mfa.controller.ts +++ b/src/lib/server/api/controllers/mfa.controller.ts @@ -7,7 +7,7 @@ import { UsersService } from '$lib/server/api/services/users.service'; import { zValidator } from '@hono/zod-validator'; import { inject, injectable } from '@needle-di/core'; 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 { decodeBase64 } from '@oslojs/encoding'; import { LoginRequestsService } from '../services/loginrequest.service'; @@ -26,12 +26,13 @@ export class MfaController extends Controller { routes() { return this.controller - .get('/totp', requireAuth, async (c) => { + .get('/totp', requireTempAuth, async (c) => { 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); 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 { key } = c.req.valid('json'); 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); }) - .delete('/totp', requireAuth, async (c) => { + .delete('/totp', requireFullAuth, async (c) => { const user = c.var.user; try { await this.totpService.deleteOneByUserIdAndType(user.id, CredentialsType.TOTP); @@ -54,20 +55,20 @@ export class MfaController extends Controller { return c.status(StatusCodes.INTERNAL_SERVER_ERROR); } }) - .get('/totp/recoveryCodes', requireAuth, async (c) => { + .get('/totp/recoveryCodes', requireFullAuth, async (c) => { const user = c.var.user; // You can only view recovery codes once and that is on creation const existingCodes = await this.recoveryCodesService.findAllRecoveryCodesByUserId(user.id); if (existingCodes && existingCodes.length > 0) { console.log('Recovery Codes found', existingCodes); // 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 }); } const recoveryCodes = await this.recoveryCodesService.createRecoveryCodes(user.id); return c.json({ recoveryCodes }); }) - .post('/totp/recoveryCodes', requireAuth, zValidator('json', verifyTotpDto), async (c) => { + .post('/totp/recoveryCodes', requireFullAuth, zValidator('json', verifyTotpDto), async (c) => { try { const user = c.var.user; const { code } = c.req.valid('json'); @@ -82,7 +83,7 @@ export class MfaController extends Controller { 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 { const user = c.var.user; const { code } = c.req.valid('json'); @@ -90,7 +91,7 @@ export class MfaController extends Controller { const verified = await this.totpService.verify(user.id, code); if (verified) { 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); console.log('set cookie', sessionCookie); setSessionCookie(c, sessionCookie); diff --git a/src/lib/server/api/controllers/signup.controller.ts b/src/lib/server/api/controllers/signup.controller.ts index ac6cb9c..2b6e0cd 100644 --- a/src/lib/server/api/controllers/signup.controller.ts +++ b/src/lib/server/api/controllers/signup.controller.ts @@ -34,7 +34,7 @@ export class SignupController extends Controller { 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); console.log('set cookie', sessionCookie); setSessionCookie(c, sessionCookie); diff --git a/src/lib/server/api/controllers/user.controller.ts b/src/lib/server/api/controllers/user.controller.ts index 090bef6..b1a2ac5 100644 --- a/src/lib/server/api/controllers/user.controller.ts +++ b/src/lib/server/api/controllers/user.controller.ts @@ -1,29 +1,30 @@ -import {Controller} from '$lib/server/api/common/types/controller'; -import {UsersService} from '$lib/server/api/services/users.service'; -import {inject, injectable} from '@needle-di/core'; -import {requireAuth} from '../middleware/require-auth.middleware'; +import { Controller } from '$lib/server/api/common/types/controller'; +import { UsersService } from '$lib/server/api/services/users.service'; +import { inject, injectable } from '@needle-di/core'; +import { requireFullAuth, requireTempAuth } from '../middleware/require-auth.middleware'; @injectable() export class UserController extends Controller { - constructor(private usersService = inject(UsersService)) { - super(); - } + constructor(private usersService = inject(UsersService)) { + super(); + } - routes() { - return this.controller - .get('/', async (c) => { - const user = c.var.user; - return c.json({ user }); - }) - .get('/:id', requireAuth, async (c) => { - const id = c.req.param('id'); - const user = await this.usersService.findOneById(id); - return c.json({ user }); - }) - .get('/username/:userName', requireAuth, async (c) => { - const userName = c.req.param('userName'); - const user = await this.usersService.findOneByUsername(userName); - return c.json({ user }); - }); - } + routes() { + return this.controller + .get('/', requireTempAuth, async (c) => { + const session = c.var.session; + const user = session ? await this.usersService.findOneById(session.userId) : null; + return c.json({ user, session }); + }) + .get('/:id', requireFullAuth, async (c) => { + const id = c.req.param('id'); + const user = await this.usersService.findOneById(id); + return c.json({ user }); + }) + .get('/username/:userName', requireFullAuth, async (c) => { + const userName = c.req.param('userName'); + const user = await this.usersService.findOneByUsername(userName); + return c.json({ user }); + }); + } } diff --git a/src/lib/server/api/controllers/wishlist.controller.ts b/src/lib/server/api/controllers/wishlist.controller.ts index e227c2d..2ba445f 100644 --- a/src/lib/server/api/controllers/wishlist.controller.ts +++ b/src/lib/server/api/controllers/wishlist.controller.ts @@ -1,25 +1,25 @@ -import {Controller} from '$lib/server/api/common/types/controller' -import {WishlistsService} from '$lib/server/api/services/wishlists.service' -import {inject, injectable} from '@needle-di/core' -import {requireAuth} from '../middleware/require-auth.middleware' +import { Controller } from '$lib/server/api/common/types/controller'; +import { WishlistsService } from '$lib/server/api/services/wishlists.service'; +import { inject, injectable } from '@needle-di/core'; +import { requireFullAuth } from '../middleware/require-auth.middleware'; @injectable() export class WishlistController extends Controller { - constructor(private wishlistsService = inject(WishlistsService)) { - super() - } + constructor(private wishlistsService = inject(WishlistsService)) { + super(); + } - routes() { - return this.controller - .get('/', requireAuth, async (c) => { - const user = c.var.user - const wishlists = await this.wishlistsService.findAllByUserId(user.id) - return c.json({ wishlists }) - }) - .get('/:cuid', requireAuth, async (c) => { - const cuid = c.req.param('cuid') - const wishlist = await this.wishlistsService.findOneByCuid(cuid) - return c.json({ wishlist }) - }) - } + routes() { + return this.controller + .get('/', requireFullAuth, async (c) => { + const user = c.var.user; + const wishlists = await this.wishlistsService.findAllByUserId(user.id); + return c.json({ wishlists }); + }) + .get('/:cuid', requireFullAuth, async (c) => { + const cuid = c.req.param('cuid'); + const wishlist = await this.wishlistsService.findOneByCuid(cuid); + return c.json({ wishlist }); + }); + } } diff --git a/src/lib/server/api/databases/postgres/seed.ts b/src/lib/server/api/databases/postgres/seed.ts index d911089..61c676b 100644 --- a/src/lib/server/api/databases/postgres/seed.ts +++ b/src/lib/server/api/databases/postgres/seed.ts @@ -36,7 +36,6 @@ for (const table of [ schema.publishers_to_games, schema.recoveryCodesTable, schema.rolesTable, - schema.sessionsTable, schema.twoFactorTable, schema.user_roles, schema.usersTable, diff --git a/src/lib/server/api/databases/postgres/tables/index.ts b/src/lib/server/api/databases/postgres/tables/index.ts index 006272a..4983150 100644 --- a/src/lib/server/api/databases/postgres/tables/index.ts +++ b/src/lib/server/api/databases/postgres/tables/index.ts @@ -18,7 +18,6 @@ export * from './publishersToExternalIds.table' export * from './publishersToGames.table' export * from './recovery-codes.table' export * from './roles.table' -export * from './sessions.table' export * from './two-factor.table' export * from './userRoles.table' export * from './users.table' diff --git a/src/lib/server/api/databases/postgres/tables/sessions.table.ts b/src/lib/server/api/databases/postgres/tables/sessions.table.ts deleted file mode 100644 index 5b783f0..0000000 --- a/src/lib/server/api/databases/postgres/tables/sessions.table.ts +++ /dev/null @@ -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; diff --git a/src/lib/server/api/middleware/require-auth.middleware.ts b/src/lib/server/api/middleware/require-auth.middleware.ts index 93c8a86..ee3993d 100644 --- a/src/lib/server/api/middleware/require-auth.middleware.ts +++ b/src/lib/server/api/middleware/require-auth.middleware.ts @@ -1,16 +1,30 @@ -import {Unauthorized} from '$lib/server/api/common/exceptions'; -import type {Sessions} from '$lib/server/api/databases/postgres/tables'; -import type {MiddlewareHandler} from 'hono'; -import {createMiddleware} from 'hono/factory'; -import type {User} from 'lucia'; +import { Unauthorized } from '$lib/server/api/common/exceptions'; +import type { Sessions, Users } from '$lib/server/api/databases/postgres/tables'; +import type { MiddlewareHandler } from 'hono'; +import { createMiddleware } from 'hono/factory'; -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: { session: Sessions; - user: User; + user: Users; }; }> = createMiddleware(async (c, next) => { - const user = c.var.user; - if (!user) throw Unauthorized('You must be logged in to access this resource'); + const session = c.var.session; + if (!session) { + throw Unauthorized('You must be logged in to access this resource'); + } return next(); -}); +}); \ No newline at end of file diff --git a/src/lib/server/api/services/loginrequest.service.ts b/src/lib/server/api/services/loginrequest.service.ts index 2f92e49..50d67b0 100644 --- a/src/lib/server/api/services/loginrequest.service.ts +++ b/src/lib/server/api/services/loginrequest.service.ts @@ -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 type { HonoRequest } from 'hono'; 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 { CredentialsRepository } from '../repositories/credentials.repository'; import { UsersRepository } from '../repositories/users.repository'; @@ -39,7 +39,7 @@ export class LoginRequestsService { const existingUser = await this.usersRepository.findOneByUsername(data.username); if (!existingUser) { - throw BadRequest('User not found'); + throw NotFound('User not found'); } const credential = await this.credentialsRepository.findPasswordCredentialsByUserId(existingUser.id); @@ -54,10 +54,15 @@ export class LoginRequestsService { 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 requestIpCountry = req.header('x-vercel-ip-country'); return this.sessionsService.createSession( @@ -66,7 +71,7 @@ export class LoginRequestsService { requestIpCountry || 'unknown', requestIpAddress || 'unknown', twoFactorAuthEnabled, - false, + twoFactorVerified, ); } diff --git a/src/lib/server/api/services/redis.service.ts b/src/lib/server/api/services/redis.service.ts index ca697b2..b2d7352 100644 --- a/src/lib/server/api/services/redis.service.ts +++ b/src/lib/server/api/services/redis.service.ts @@ -5,15 +5,36 @@ import type {Disposable} from 'tsyringe'; @injectable() export class RedisService implements Disposable { - readonly client: Redis + readonly client: Redis; - constructor() { - this.client = new Redis(config.redis.url, { - maxRetriesPerRequest: null, - }) - } + constructor() { + this.client = new Redis(config.redis.url, { + maxRetriesPerRequest: null, + }); + } - async dispose(): Promise { - this.client.disconnect() - } + async get(data: { prefix: string; key: string }): Promise { + return this.client.get(`${data.prefix}:${data.key}`); + } + + async set(data: { prefix: string; key: string; value: string }): Promise { + await this.client.set(`${data.prefix}:${data.key}`, data.value); + } + + async delete(data: { prefix: string; key: string }): Promise { + await this.client.del(`${data.prefix}:${data.key}`); + } + + async setWithExpiry(data: { + prefix: string; + key: string; + value: string; + expiry: number; + }): Promise { + await this.client.set(`${data.prefix}:${data.key}`, data.value, 'EXAT', Math.floor(data.expiry)); + } + + async dispose(): Promise { + this.client.disconnect(); + } } diff --git a/src/lib/server/api/services/sessions.service.ts b/src/lib/server/api/services/sessions.service.ts index 0392988..b08a14e 100644 --- a/src/lib/server/api/services/sessions.service.ts +++ b/src/lib/server/api/services/sessions.service.ts @@ -17,119 +17,119 @@ export type RedisSession = { }; export type Session = { - id: string; - userId: string; - expiresAt: Date; - ipCountry: string; - ipAddress: string; - twoFactorAuthEnabled: boolean; - isTwoFactorAuthenticated: boolean; + id: string; + userId: string; + expiresAt: Date; + ipCountry: string; + ipAddress: string; + twoFactorEnabled: boolean; + twoFactorVerified: boolean; }; export type SessionValidationResult = { session: Session; user: Users } | { session: null; user: null } | { session: Session; user: undefined }; @injectable() export class SessionsService { - constructor( - private redisService = inject(RedisService), - private usersRepository = inject(UsersRepository), - ) {} + constructor( + private redisService = inject(RedisService), + private usersRepository = inject(UsersRepository), + ) {} - generateSessionToken() { - const bytes = new Uint8Array(20); - crypto.getRandomValues(bytes); - return encodeBase32LowerCaseNoPadding(bytes); - } + generateSessionToken() { + const bytes = new Uint8Array(20); + crypto.getRandomValues(bytes); + return encodeBase32LowerCaseNoPadding(bytes); + } - async createSession( - token: string, - userId: string, - ipCountry: string, - ipAddress: string, - twoFactorAuthEnabled: boolean, - isTwoFactorAuthenticated: boolean, - ) { - const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); - const session = { - id: sessionId, - userId, - expiresAt: cookieExpiresAt, - ipCountry, - ipAddress, - twoFactorAuthEnabled, - isTwoFactorAuthenticated, - }; - await this.redisService.client.set( - `session:${sessionId}`, - JSON.stringify({ - id: session.id, - user_id: session.userId, - expires_at: session.expiresAt, - ip_country: session.ipCountry, - ip_address: session.ipAddress, - two_factor_auth_enabled: session.twoFactorAuthEnabled, - is_two_factor_authenticated: session.isTwoFactorAuthenticated, - }), - 'EXAT', - Math.floor(session.expiresAt.getTime() / 1000), - ); - return session; - } + async createSession( + token: string, + userId: string, + ipCountry: string, + ipAddress: string, + twoFactorAuthEnabled: boolean, + twoFactorVerified: boolean, + ) { + const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); + const session = { + id: sessionId, + userId, + expiresAt: cookieExpiresAt, + ipCountry, + ipAddress, + twoFactorAuthEnabled, + twoFactorVerified, + }; + await this.redisService.client.set( + `session:${sessionId}`, + JSON.stringify({ + id: session.id, + user_id: session.userId, + expires_at: session.expiresAt, + ip_country: session.ipCountry, + ip_address: session.ipAddress, + two_factor_auth_enabled: session.twoFactorAuthEnabled, + is_two_factor_authenticated: session.twoFactorVerified, + }), + 'EXAT', + Math.floor(session.expiresAt.getTime() / 1000), + ); + return session; + } - async validateSessionToken(token: string): Promise { - // 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 item = await this.redisService.client.get(`session:${token}`); - if (item === null) { - return { - session: null, - user: null, - }; - } - const result: RedisSession = JSON.parse(item); - const session: Session = { - id: result.id, - userId: result.user_id, - expiresAt: new Date(result.expires_at * 1000), - ipCountry: result.ip_country, - ipAddress: result.ip_address, - twoFactorAuthEnabled: result.two_factor_auth_enabled, - isTwoFactorAuthenticated: result.is_two_factor_authenticated, - }; - let user: Users | undefined = undefined; - if (session.userId) { - user = await this.usersRepository.findOneById(session.userId); - } - if (Date.now() >= session.expiresAt.getTime()) { - await this.redisService.client.del(`session:${token}`); - return { - session: null, - user: null, - }; - } + async validateSessionToken(token: string): Promise { + // 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 item = await this.redisService.client.get(`session:${token}`); + if (item === null) { + return { + session: null, + user: null, + }; + } + const result: RedisSession = JSON.parse(item); + const session: Session = { + id: result.id, + userId: result.user_id, + expiresAt: new Date(result.expires_at * 1000), + ipCountry: result.ip_country, + ipAddress: result.ip_address, + twoFactorEnabled: result.two_factor_auth_enabled, + twoFactorVerified: result.is_two_factor_authenticated, + }; + let user: Users | undefined = undefined; + if (session.userId) { + user = await this.usersRepository.findOneById(session.userId); + } + if (Date.now() >= session.expiresAt.getTime()) { + await this.redisService.client.del(`session:${token}`); + return { + session: null, + user: null, + }; + } - if (Date.now() >= session.expiresAt.getTime() - cookieExpiresMilliseconds) { - session.expiresAt = new Date(Date.now() + halfCookieExpiresMilliseconds); - await this.redisService.client.set( - `session:${token}`, - JSON.stringify({ - id: session.id, - user_id: session.userId, - expires_at: Math.floor(session.expiresAt.getTime() / 1000), - ip_country: session.ipCountry, - ip_address: session.ipAddress, - two_factor_auth_enabled: session.twoFactorAuthEnabled, - is_two_factor_authenticated: session.isTwoFactorAuthenticated, - }), - 'EXAT', - Math.floor(session.expiresAt.getTime() / 1000), - ); - } + if (Date.now() >= session.expiresAt.getTime() - cookieExpiresMilliseconds) { + session.expiresAt = new Date(Date.now() + halfCookieExpiresMilliseconds); + await this.redisService.client.set( + `session:${token}`, + JSON.stringify({ + id: session.id, + user_id: session.userId, + expires_at: Math.floor(session.expiresAt.getTime() / 1000), + ip_country: session.ipCountry, + ip_address: session.ipAddress, + two_factor_enabled: session.twoFactorEnabled, + two_factor_verified: session.twoFactorVerified, + }), + 'EXAT', + Math.floor(session.expiresAt.getTime() / 1000), + ); + } - return { session, user }; - } + return { session, user }; + } - async invalidateSession(sessionId: string) { - await this.redisService.client.del(`session:${sessionId}`); - } + async invalidateSession(sessionId: string) { + await this.redisService.client.del(`session:${sessionId}`); + } } diff --git a/src/lib/server/auth-utils.ts b/src/lib/server/auth-utils.ts index ca6220c..820a8fc 100644 --- a/src/lib/server/auth-utils.ts +++ b/src/lib/server/auth-utils.ts @@ -1,19 +1,20 @@ -import {eq} from 'drizzle-orm'; -import {generateIdFromEntropySize, type Session, type User} from 'lucia'; -import {createDate, TimeSpan} from 'oslo'; -import {password_reset_tokens} from './api/databases/postgres/tables'; -import {db} from './api/packages/drizzle'; +import { eq } from 'drizzle-orm'; +import { generateIdFromEntropySize } from 'lucia'; +import { createDate, TimeSpan } from 'oslo'; +import { password_reset_tokens, type Users } from './api/databases/postgres/tables'; +import { db } from './api/packages/drizzle'; +import type { Session } from './api/services/sessions.service'; export async function createPasswordResetToken(userId: string): Promise { - // optionally invalidate all existing tokens - await db.delete(password_reset_tokens).where(eq(password_reset_tokens.user_id, userId)); - const tokenId = generateIdFromEntropySize(40); - await db.insert(password_reset_tokens).values({ - id: tokenId, - user_id: userId, - expires_at: createDate(new TimeSpan(2, 'h')), - }); - return tokenId; + // optionally invalidate all existing tokens + await db.delete(password_reset_tokens).where(eq(password_reset_tokens.user_id, userId)); + const tokenId = generateIdFromEntropySize(40); + await db.insert(password_reset_tokens).values({ + id: tokenId, + user_id: userId, + expires_at: createDate(new TimeSpan(2, 'h')), + }); + return tokenId; } /** @@ -23,8 +24,8 @@ export async function createPasswordResetToken(userId: string): Promise * @param session - The session object. * @returns True if the user is not fully authenticated, otherwise false. */ -export function userNotFullyAuthenticated(user: User | null, session: Session | null) { - return user && session && session.isTwoFactorAuthEnabled && !session.isTwoFactorAuthenticated; +export function userNotFullyAuthenticated(user: Users | null, session: Session | null) { + 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. */ 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. */ export function userFullyAuthenticated(user: User | null, session: Session | null) { - return !userNotAuthenticated(user, session); + return !userNotAuthenticated(user, session); } diff --git a/src/routes/(app)/(protected)/+layout.server.ts b/src/routes/(app)/(protected)/+layout.server.ts new file mode 100644 index 0000000..2d740a7 --- /dev/null +++ b/src/routes/(app)/(protected)/+layout.server.ts @@ -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, + }, + }; +}); diff --git a/src/routes/(app)/(protected)/+page.server.ts b/src/routes/(app)/(protected)/+page.server.ts new file mode 100644 index 0000000..090812c --- /dev/null +++ b/src/routes/(app)/(protected)/+page.server.ts @@ -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, + }; +}; diff --git a/src/routes/(app)/+page.svelte b/src/routes/(app)/(protected)/+page.svelte similarity index 53% rename from src/routes/(app)/+page.svelte rename to src/routes/(app)/(protected)/+page.svelte index fd7907b..60bfb84 100644 --- a/src/routes/(app)/+page.svelte +++ b/src/routes/(app)/(protected)/+page.svelte @@ -1,22 +1,25 @@
@@ -37,7 +40,10 @@ const welcomeName = $derived.by(() => { {:else}

Welcome to Bored Game!

Track the board games you own, the ones you want, and whether you play them enough.

-

Get started by joining the wait list or log in if you already have an account.

+

+ Get started by joining the wait list or log in if you already have an + account. +

{/if}
diff --git a/src/routes/(app)/game/[id]/+error.svelte b/src/routes/(app)/(protected)/game/[id]/+error.svelte similarity index 100% rename from src/routes/(app)/game/[id]/+error.svelte rename to src/routes/(app)/(protected)/game/[id]/+error.svelte diff --git a/src/routes/(app)/game/[id]/+page.server.ts b/src/routes/(app)/(protected)/game/[id]/+page.server.ts similarity index 100% rename from src/routes/(app)/game/[id]/+page.server.ts rename to src/routes/(app)/(protected)/game/[id]/+page.server.ts diff --git a/src/routes/(app)/game/[id]/+page.svelte b/src/routes/(app)/(protected)/game/[id]/+page.svelte similarity index 100% rename from src/routes/(app)/game/[id]/+page.svelte rename to src/routes/(app)/(protected)/game/[id]/+page.svelte diff --git a/src/routes/(app)/search/+error.svelte b/src/routes/(app)/(protected)/search/+error.svelte similarity index 100% rename from src/routes/(app)/search/+error.svelte rename to src/routes/(app)/(protected)/search/+error.svelte diff --git a/src/routes/(app)/search/+page.server.ts b/src/routes/(app)/(protected)/search/+page.server.ts similarity index 100% rename from src/routes/(app)/search/+page.server.ts rename to src/routes/(app)/(protected)/search/+page.server.ts diff --git a/src/routes/(app)/search/+page.svelte b/src/routes/(app)/(protected)/search/+page.svelte similarity index 100% rename from src/routes/(app)/search/+page.svelte rename to src/routes/(app)/(protected)/search/+page.svelte diff --git a/src/routes/(app)/about/+page.svelte b/src/routes/(app)/(public)/about/+page.svelte similarity index 100% rename from src/routes/(app)/about/+page.svelte rename to src/routes/(app)/(public)/about/+page.svelte diff --git a/src/routes/(app)/(public)/landing/+page.server.ts b/src/routes/(app)/(public)/landing/+page.server.ts new file mode 100644 index 0000000..4b245a1 --- /dev/null +++ b/src/routes/(app)/(public)/landing/+page.server.ts @@ -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, + }; +}; diff --git a/src/routes/(app)/(public)/landing/+page.svelte b/src/routes/(app)/(public)/landing/+page.svelte new file mode 100644 index 0000000..6e5fe50 --- /dev/null +++ b/src/routes/(app)/(public)/landing/+page.svelte @@ -0,0 +1,26 @@ + + +
+

Welcome to Bored Game!

+

Track the board games you own, the ones you want, and whether you play them enough.

+

+ Get started by joining the wait list or log in if you already have an account. +

+
+ + diff --git a/src/routes/(app)/privacy/+page.svelte b/src/routes/(app)/(public)/privacy/+page.svelte similarity index 100% rename from src/routes/(app)/privacy/+page.svelte rename to src/routes/(app)/(public)/privacy/+page.svelte diff --git a/src/routes/(app)/terms/+page.svelte b/src/routes/(app)/(public)/terms/+page.svelte similarity index 100% rename from src/routes/(app)/terms/+page.svelte rename to src/routes/(app)/(public)/terms/+page.svelte diff --git a/src/routes/(app)/waitlist/+page.svelte b/src/routes/(app)/(public)/waitlist/+page.svelte similarity index 100% rename from src/routes/(app)/waitlist/+page.svelte rename to src/routes/(app)/(public)/waitlist/+page.svelte diff --git a/src/routes/(app)/+layout.server.ts b/src/routes/(app)/+layout.server.ts index 8b60c88..2c1f1d4 100644 --- a/src/routes/(app)/+layout.server.ts +++ b/src/routes/(app)/+layout.server.ts @@ -3,10 +3,15 @@ import type { LayoutServerLoad } from '../$types'; export const load: LayoutServerLoad = loadFlash(async (event) => { const { url, locals } = event; - const authedUser = await locals.getAuthedUser(); + const { user } = await locals.getAuthedUser(); return { url: url.pathname, - authedUser, + user: user ? { + cuid: user?.cuid, + firstName: user?.firstName, + lastName: user?.lastName, + username: user?.username, + } : null, }; }); diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 6438179..54d3970 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -7,7 +7,7 @@ const { data, children } = $props();
-
+
{ - 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: [] }; -}; diff --git a/src/routes/(app)/about/+page.ts b/src/routes/(app)/about/+page.ts deleted file mode 100644 index 3e13462..0000000 --- a/src/routes/(app)/about/+page.ts +++ /dev/null @@ -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; diff --git a/src/routes/(app)/privacy/+page.server.ts b/src/routes/(app)/privacy/+page.server.ts deleted file mode 100644 index 189f71e..0000000 --- a/src/routes/(app)/privacy/+page.server.ts +++ /dev/null @@ -1 +0,0 @@ -export const prerender = true; diff --git a/src/routes/(app)/terms/+page.server.ts b/src/routes/(app)/terms/+page.server.ts deleted file mode 100644 index 189f71e..0000000 --- a/src/routes/(app)/terms/+page.server.ts +++ /dev/null @@ -1 +0,0 @@ -export const prerender = true; diff --git a/src/routes/(auth)/+layout.server.ts b/src/routes/(auth)/+layout.server.ts index 56daa2a..2a2b1e9 100644 --- a/src/routes/(auth)/+layout.server.ts +++ b/src/routes/(auth)/+layout.server.ts @@ -1,8 +1,11 @@ +import { redirect } from '@sveltejs/kit'; + export async function load(event) { const { url, locals } = event; + const { user, session } = await locals.getAuthedUser(); return { url: url.pathname, - user: locals.user, + user, }; } diff --git a/src/routes/(auth)/login/+page.server.ts b/src/routes/(auth)/login/+page.server.ts index 301a605..53bd2f8 100644 --- a/src/routes/(auth)/login/+page.server.ts +++ b/src/routes/(auth)/login/+page.server.ts @@ -9,9 +9,9 @@ import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async (event) => { const { locals } = event; - const authedUser = await locals.getAuthedUser(); + const { user } = await locals.getAuthedUser(); - if (authedUser) { + if (user) { console.log('user already signed in'); const message = { type: 'success', message: 'You are already signed in' } as const; throw redirect('/', message, event); @@ -28,9 +28,9 @@ export const actions: Actions = { default: async (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; throw redirect('/', message, event); } @@ -38,8 +38,10 @@ export const actions: Actions = { const form = await superValidate(event, zod(signinUsernameDto)); const { error } = await locals.api.login.$post({ json: form.data }).then(locals.parseApiResponse); + console.log('Login error', 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) { diff --git a/src/routes/(auth)/signup/+page.server.ts b/src/routes/(auth)/signup/+page.server.ts index 801191f..9790f4c 100644 --- a/src/routes/(auth)/signup/+page.server.ts +++ b/src/routes/(auth)/signup/+page.server.ts @@ -6,149 +6,59 @@ import { setError, superValidate } from 'sveltekit-superforms/server'; import type { PageServerLoad } from './$types'; const signUpDefaults = { - firstName: '', - lastName: '', - email: '', - username: '', - password: '', - confirm_password: '', - terms: true, + firstName: '', + lastName: '', + email: '', + username: '', + password: '', + confirm_password: '', + terms: true, }; export const load: PageServerLoad = async (event) => { - const { locals } = event; + const { locals } = event; - const authedUser = await locals.getAuthedUser(); + const { user } = await locals.getAuthedUser(); - if (authedUser) { - const message = { type: 'success', message: 'You are already signed in' } as const; - throw redirect('/', message, event); - } + if (user) { + const message = { type: 'success', message: 'You are already signed in' } as const; + throw redirect('/', message, event); + } - // if (userFullyAuthenticated(user, session)) { - // const message = { type: 'success', message: 'You are already signed in' } as const; - // throw redirect('/', message, event); - // } 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, - }), - }; + return { + signupForm: await superValidate(zod(signupUsernameEmailDto), { + defaults: signUpDefaults, + }), + }; }; export const actions: Actions = { - default: async (event) => { - const { locals } = event; + default: async (event) => { + const { locals } = event; - const authedUser = await locals.getAuthedUser(); + const { user } = await locals.getAuthedUser(); - if (authedUser) { - const message = { type: 'success', message: 'You are already signed in' } as const; - throw redirect('/', message, event); - } + if (user) { + const message = { type: 'success', message: 'You are already signed in' } as const; + 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); - if (error) return setError(form, 'username', error); - - if (!form.valid) { + const { error } = await locals.api.signup.$post({ json: form.data }).then(locals.parseApiResponse); + if (error) { form.data.password = ''; - form.data.confirm_password = ''; - return fail(400, { - form, - }); - } + return setError(form, 'username', 'Unable to log in.'); + } - // let session; - // let sessionCookie; - // // Adding user to the db - // console.log('Check if user already exists'); - // - // 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, - // }); + if (!form.valid) { + form.data.password = ''; + form.data.confirm_password = ''; + return fail(400, { + form, + }); + } - redirect(302, '/'); - // const message = { type: 'success', message: 'Signed Up!' } as const; - // throw flashRedirect(message, event); - }, + redirect(302, '/'); + }, }; diff --git a/src/routes/(auth)/totp/+page.server.ts b/src/routes/(auth)/totp/+page.server.ts index 098279a..3317765 100644 --- a/src/routes/(auth)/totp/+page.server.ts +++ b/src/routes/(auth)/totp/+page.server.ts @@ -13,19 +13,16 @@ import type { PageServerLoad, RequestEvent } from './$types'; export const load: PageServerLoad = async (event) => { const { locals } = event; - - const authedUser = await locals.getAuthedUser(); - if (!authedUser) { + const { session } = await locals.getAuthedUser(); + if (session === null) { throw redirect(302, '/login', notSignedInMessage, event); } - - const { data } = await locals.api.mfa.totp.$get().then(locals.parseApiResponse); - if (!data) { + if (!session?.twoFactorEnabled) { throw redirect(302, '/login', notSignedInMessage, event); } - const { totpCredential } = data; - if (!totpCredential) { - 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); } return { @@ -38,10 +35,14 @@ export const actions: Actions = { validateTotp: async (event) => { const { locals } = event; - const authedUser = await locals.getAuthedUser(); - if (!authedUser) { + const { session } = await locals.getAuthedUser(); + if (session === null) { 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); if (!totpData) { @@ -64,16 +65,24 @@ export const actions: Actions = { return setError(totpForm, 'code', totpVerifyError); } - console.log('Successfully logged in'); - return message(totpForm, { type: 'success', message: 'Successfully logged in!' }); + totpForm.data.code = ''; + const message = { type: 'success', message: 'Successfully logged in!' } as const; + redirect(302, '/', message, event); }, validateRecoveryCode: async (event) => { const { cookies, locals } = event; - const authedUser = await locals.getAuthedUser(); - if (!authedUser) { + const { session } = await locals.getAuthedUser(); + if (session === null) { 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); @@ -157,7 +166,7 @@ async function validateUserData(event: RequestEvent, locals: App.Locals) { throw fail(401); } - const isTwoFactorAuthenticated = session?.isTwoFactorAuthenticated; + const twoFactorVerified = session?.twoFactorVerified; const twoFactorDetails = await db.query.twoFactorTable.findFirst({ where: eq(twoFactorTable.userId, dbUser!.id!), }); @@ -167,7 +176,7 @@ async function validateUserData(event: RequestEvent, locals: App.Locals) { 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; throw redirect('/', message, event); } diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index c9983c9..dc06fb9 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -1,9 +1,11 @@ import { loadFlash } from 'sveltekit-flash-message/server'; import type { LayoutServerLoad } from './$types'; +import { redirect } from '@sveltejs/kit'; export const load: LayoutServerLoad = loadFlash(async (event) => { const { locals, url } = event; - const user = await locals.getAuthedUser(); + const { user } = await locals.getAuthedUser(); + return { url: url.pathname, user,