Refactor to how v2 Tofustack works.

This commit is contained in:
Bradley Shellnut 2024-12-01 15:34:04 -08:00
parent 85050d0ec4
commit 20e37c4f18
117 changed files with 3611 additions and 3276 deletions

View file

@ -10,6 +10,10 @@ DATABASE_PASSWORD='postgres'
DATABASE_HOST='localhost' DATABASE_HOST='localhost'
DATABASE_PORT=5432 DATABASE_PORT=5432
DATABASE_DB='postgres' DATABASE_DB='postgres'
PORT=5173
ENV=dev
SIGNING_SECRET=""
ENCRYPTION_KEY="" ENCRYPTION_KEY=""
REDIS_URL='redis://127.0.0.1:6379/0' REDIS_URL='redis://127.0.0.1:6379/0'

View file

@ -30,22 +30,21 @@
"@playwright/test": "^1.49.0", "@playwright/test": "^1.49.0",
"@sveltejs/adapter-auto": "^3.3.1", "@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/enhanced-img": "^0.3.10", "@sveltejs/enhanced-img": "^0.3.10",
"@sveltejs/kit": "^2.8.2", "@sveltejs/kit": "^2.8.5",
"@sveltejs/vite-plugin-svelte": "4.0.0-next.7", "@sveltejs/vite-plugin-svelte": "4.0.0-next.7",
"@types/cookie": "^0.6.0", "@types/cookie": "^0.6.0",
"@types/node": "^20.17.7", "@types/node": "^20.17.8",
"@types/pg": "^8.11.10", "@types/pg": "^8.11.10",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
"@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0", "@typescript-eslint/parser": "^7.18.0",
"arctic": "^1.9.2", "arctic": "^2.3.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"bits-ui": "^0.21.16", "bits-ui": "^0.21.16",
"drizzle-kit": "^0.27.2", "drizzle-kit": "^0.27.2",
"formsnap": "^1.0.1", "formsnap": "^1.0.1",
"just-clone": "^6.2.0", "just-clone": "^6.2.0",
"just-debounce-it": "^3.2.0", "just-debounce-it": "^3.2.0",
"lucia": "3.2.0",
"lucide-svelte": "^0.408.0", "lucide-svelte": "^0.408.0",
"mode-watcher": "^0.4.1", "mode-watcher": "^0.4.1",
"nodemailer": "^6.9.16", "nodemailer": "^6.9.16",
@ -53,7 +52,7 @@
"postcss-import": "^16.1.0", "postcss-import": "^16.1.0",
"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.4.1",
"prettier-plugin-svelte": "^3.3.2", "prettier-plugin-svelte": "^3.3.2",
"svelte": "5.0.0-next.175", "svelte": "5.0.0-next.175",
"svelte-check": "^3.8.6", "svelte-check": "^3.8.6",
@ -96,22 +95,24 @@
"@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.161", "@scalar/hono-api-reference": "^0.5.162",
"@sveltejs/adapter-node": "^5.2.9", "@sveltejs/adapter-node": "^5.2.9",
"@sveltejs/adapter-vercel": "^5.4.8", "@sveltejs/adapter-vercel": "^5.4.8",
"@types/feather-icons": "^4.29.4", "@types/feather-icons": "^4.29.4",
"argon2": "^0.41.1",
"boardgamegeekclient": "^1.9.1", "boardgamegeekclient": "^1.9.1",
"bullmq": "^5.29.1", "bullmq": "^5.29.1",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cookie": "^1.0.2", "cookie": "^1.0.2",
"dayjs": "^1.11.13",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"dotenv-expand": "^11.0.7", "dotenv-expand": "^11.0.7",
"drizzle-orm": "^0.36.4", "drizzle-orm": "^0.36.4",
"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.11", "hono": "^4.6.12",
"hono-pino": "^0.7.0", "hono-pino": "^0.7.0",
"hono-rate-limiter": "^0.4.0", "hono-rate-limiter": "^0.4.0",
"hono-zod-openapi": "^0.5.0", "hono-zod-openapi": "^0.5.0",
@ -121,9 +122,8 @@
"just-capitalize": "^3.2.0", "just-capitalize": "^3.2.0",
"just-kebab-case": "^4.2.0", "just-kebab-case": "^4.2.0",
"loader": "^2.1.1", "loader": "^2.1.1",
"nanoid": "^5.0.8", "nanoid": "^5.0.9",
"open-props": "^1.7.7", "open-props": "^1.7.7",
"oslo": "^1.2.1",
"pg": "^8.13.1", "pg": "^8.13.1",
"pino": "^9.5.0", "pino": "^9.5.0",
"pino-pretty": "^11.3.0", "pino-pretty": "^11.3.0",
@ -132,12 +132,11 @@
"radix-svelte": "^0.9.0", "radix-svelte": "^0.9.0",
"rate-limit-redis": "^4.2.0", "rate-limit-redis": "^4.2.0",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"stoker": "^1.3.0", "stoker": "^1.4.2",
"svelte-lazy-loader": "^1.0.0", "svelte-lazy-loader": "^1.0.0",
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.5.5",
"tailwind-variants": "^0.2.1", "tailwind-variants": "^0.2.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"tsyringe": "^4.8.0",
"zod-to-json-schema": "^3.23.5" "zod-to-json-schema": "^3.23.5"
} }
} }

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.11) version: 0.4.1(hono@4.6.12)
'@hono/zod-openapi': '@hono/zod-openapi':
specifier: ^0.15.3 specifier: ^0.15.3
version: 0.15.3(hono@4.6.11)(zod@3.23.8) version: 0.15.3(hono@4.6.12)(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.11)(zod@3.23.8) version: 0.2.2(hono@4.6.12)(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
@ -28,7 +28,7 @@ importers:
version: 1.2.48 version: 1.2.48
'@inlang/paraglide-sveltekit': '@inlang/paraglide-sveltekit':
specifier: ^0.11.1 specifier: ^0.11.1
version: 0.11.5(@sveltejs/kit@2.8.2(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7))) version: 0.11.5(@sveltejs/kit@2.8.5(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))
'@internationalized/date': '@internationalized/date':
specifier: ^3.6.0 specifier: ^3.6.0
version: 3.6.0 version: 3.6.0
@ -72,17 +72,20 @@ 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.161 specifier: ^0.5.162
version: 0.5.161(hono@4.6.11) version: 0.5.162(hono@4.6.12)
'@sveltejs/adapter-node': '@sveltejs/adapter-node':
specifier: ^5.2.9 specifier: ^5.2.9
version: 5.2.9(@sveltejs/kit@2.8.2(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7))) version: 5.2.9(@sveltejs/kit@2.8.5(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))
'@sveltejs/adapter-vercel': '@sveltejs/adapter-vercel':
specifier: ^5.4.8 specifier: ^5.4.8
version: 5.4.8(@sveltejs/kit@2.8.2(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7))) version: 5.4.8(@sveltejs/kit@2.8.5(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))
'@types/feather-icons': '@types/feather-icons':
specifier: ^4.29.4 specifier: ^4.29.4
version: 4.29.4 version: 4.29.4
argon2:
specifier: ^0.41.1
version: 0.41.1
boardgamegeekclient: boardgamegeekclient:
specifier: ^1.9.1 specifier: ^1.9.1
version: 1.9.1 version: 1.9.1
@ -90,14 +93,17 @@ importers:
specifier: ^5.29.1 specifier: ^5.29.1
version: 5.29.1 version: 5.29.1
class-variance-authority: class-variance-authority:
specifier: ^0.7.0 specifier: ^0.7.1
version: 0.7.0 version: 0.7.1
clsx: clsx:
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1 version: 2.1.1
cookie: cookie:
specifier: ^1.0.2 specifier: ^1.0.2
version: 1.0.2 version: 1.0.2
dayjs:
specifier: ^1.11.13
version: 1.11.13
dotenv: dotenv:
specifier: ^16.4.5 specifier: ^16.4.5
version: 16.4.5 version: 16.4.5
@ -117,17 +123,17 @@ importers:
specifier: ^4.7.8 specifier: ^4.7.8
version: 4.7.8 version: 4.7.8
hono: hono:
specifier: ^4.6.11 specifier: ^4.6.12
version: 4.6.11 version: 4.6.12
hono-pino: hono-pino:
specifier: ^0.7.0 specifier: ^0.7.0
version: 0.7.0(hono@4.6.11)(pino@9.5.0) version: 0.7.0(hono@4.6.12)(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.11) version: 0.4.0(hono@4.6.12)
hono-zod-openapi: hono-zod-openapi:
specifier: ^0.5.0 specifier: ^0.5.0
version: 0.5.0(hono@4.6.11)(zod@3.23.8) version: 0.5.0(hono@4.6.12)(zod@3.23.8)
html-entities: html-entities:
specifier: ^2.5.2 specifier: ^2.5.2
version: 2.5.2 version: 2.5.2
@ -147,14 +153,11 @@ importers:
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1 version: 2.1.1
nanoid: nanoid:
specifier: ^5.0.8 specifier: ^5.0.9
version: 5.0.8 version: 5.0.9
open-props: open-props:
specifier: ^1.7.7 specifier: ^1.7.7
version: 1.7.7 version: 1.7.7
oslo:
specifier: ^1.2.1
version: 1.2.1
pg: pg:
specifier: ^8.13.1 specifier: ^8.13.1
version: 8.13.1 version: 8.13.1
@ -180,8 +183,8 @@ importers:
specifier: ^0.2.2 specifier: ^0.2.2
version: 0.2.2 version: 0.2.2
stoker: stoker:
specifier: ^1.3.0 specifier: ^1.4.2
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) version: 1.4.2(@asteasolutions/zod-to-openapi@7.1.2(zod@3.23.8))(@hono/zod-openapi@0.15.3(hono@4.6.12)(zod@3.23.8))(hono@4.6.12)(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
@ -190,13 +193,10 @@ importers:
version: 2.5.5 version: 2.5.5
tailwind-variants: tailwind-variants:
specifier: ^0.2.1 specifier: ^0.2.1
version: 0.2.1(tailwindcss@3.4.15(ts-node@10.9.2(@types/node@20.17.7)(typescript@5.7.2))) version: 0.2.1(tailwindcss@3.4.15(ts-node@10.9.2(@types/node@20.17.8)(typescript@5.7.2)))
tailwindcss-animate: tailwindcss-animate:
specifier: ^1.0.7 specifier: ^1.0.7
version: 1.0.7(tailwindcss@3.4.15(ts-node@10.9.2(@types/node@20.17.7)(typescript@5.7.2))) version: 1.0.7(tailwindcss@3.4.15(ts-node@10.9.2(@types/node@20.17.8)(typescript@5.7.2)))
tsyringe:
specifier: ^4.8.0
version: 4.8.0
zod-to-json-schema: zod-to-json-schema:
specifier: ^3.23.5 specifier: ^3.23.5
version: 3.23.5(zod@3.23.8) version: 3.23.5(zod@3.23.8)
@ -218,22 +218,22 @@ importers:
version: 1.49.0 version: 1.49.0
'@sveltejs/adapter-auto': '@sveltejs/adapter-auto':
specifier: ^3.3.1 specifier: ^3.3.1
version: 3.3.1(@sveltejs/kit@2.8.2(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7))) version: 3.3.1(@sveltejs/kit@2.8.5(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))
'@sveltejs/enhanced-img': '@sveltejs/enhanced-img':
specifier: ^0.3.10 specifier: ^0.3.10
version: 0.3.10(rollup@4.24.0)(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)) version: 0.3.10(rollup@4.24.0)(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8))
'@sveltejs/kit': '@sveltejs/kit':
specifier: ^2.8.2 specifier: ^2.8.5
version: 2.8.2(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)) version: 2.8.5(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8))
'@sveltejs/vite-plugin-svelte': '@sveltejs/vite-plugin-svelte':
specifier: 4.0.0-next.7 specifier: 4.0.0-next.7
version: 4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)) version: 4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8))
'@types/cookie': '@types/cookie':
specifier: ^0.6.0 specifier: ^0.6.0
version: 0.6.0 version: 0.6.0
'@types/node': '@types/node':
specifier: ^20.17.7 specifier: ^20.17.8
version: 20.17.7 version: 20.17.8
'@types/pg': '@types/pg':
specifier: ^8.11.10 specifier: ^8.11.10
version: 8.11.10 version: 8.11.10
@ -247,8 +247,8 @@ importers:
specifier: ^7.18.0 specifier: ^7.18.0
version: 7.18.0(eslint@8.57.1)(typescript@5.7.2) version: 7.18.0(eslint@8.57.1)(typescript@5.7.2)
arctic: arctic:
specifier: ^1.9.2 specifier: ^2.3.0
version: 1.9.2 version: 2.3.0
autoprefixer: autoprefixer:
specifier: ^10.4.20 specifier: ^10.4.20
version: 10.4.20(postcss@8.4.49) version: 10.4.20(postcss@8.4.49)
@ -260,16 +260,13 @@ importers:
version: 0.27.2 version: 0.27.2
formsnap: formsnap:
specifier: ^1.0.1 specifier: ^1.0.1
version: 1.0.1(svelte@5.0.0-next.175)(sveltekit-superforms@2.20.1(@sveltejs/kit@2.8.2(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(@types/json-schema@7.0.15)(svelte@5.0.0-next.175)(typescript@5.7.2)) version: 1.0.1(svelte@5.0.0-next.175)(sveltekit-superforms@2.20.1(@sveltejs/kit@2.8.5(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(@types/json-schema@7.0.15)(svelte@5.0.0-next.175)(typescript@5.7.2))
just-clone: just-clone:
specifier: ^6.2.0 specifier: ^6.2.0
version: 6.2.0 version: 6.2.0
just-debounce-it: just-debounce-it:
specifier: ^3.2.0 specifier: ^3.2.0
version: 3.2.0 version: 3.2.0
lucia:
specifier: 3.2.0
version: 3.2.0
lucide-svelte: lucide-svelte:
specifier: ^0.408.0 specifier: ^0.408.0
version: 0.408.0(svelte@5.0.0-next.175) version: 0.408.0(svelte@5.0.0-next.175)
@ -292,11 +289,11 @@ importers:
specifier: ^9.6.0 specifier: ^9.6.0
version: 9.6.0(postcss@8.4.49) version: 9.6.0(postcss@8.4.49)
prettier: prettier:
specifier: ^3.3.3 specifier: ^3.4.1
version: 3.3.3 version: 3.4.1
prettier-plugin-svelte: prettier-plugin-svelte:
specifier: ^3.3.2 specifier: ^3.3.2
version: 3.3.2(prettier@3.3.3)(svelte@5.0.0-next.175) version: 3.3.2(prettier@3.4.1)(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
@ -320,16 +317,16 @@ importers:
version: 0.3.28(svelte@5.0.0-next.175) version: 0.3.28(svelte@5.0.0-next.175)
sveltekit-flash-message: sveltekit-flash-message:
specifier: ^2.4.4 specifier: ^2.4.4
version: 2.4.4(@sveltejs/kit@2.8.2(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(svelte@5.0.0-next.175) version: 2.4.4(@sveltejs/kit@2.8.5(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(svelte@5.0.0-next.175)
sveltekit-superforms: sveltekit-superforms:
specifier: ^2.20.1 specifier: ^2.20.1
version: 2.20.1(@sveltejs/kit@2.8.2(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(@types/json-schema@7.0.15)(svelte@5.0.0-next.175)(typescript@5.7.2) version: 2.20.1(@sveltejs/kit@2.8.5(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(@types/json-schema@7.0.15)(svelte@5.0.0-next.175)(typescript@5.7.2)
tailwindcss: tailwindcss:
specifier: ^3.4.15 specifier: ^3.4.15
version: 3.4.15(ts-node@10.9.2(@types/node@20.17.7)(typescript@5.7.2)) version: 3.4.15(ts-node@10.9.2(@types/node@20.17.8)(typescript@5.7.2))
ts-node: ts-node:
specifier: ^10.9.2 specifier: ^10.9.2
version: 10.9.2(@types/node@20.17.7)(typescript@5.7.2) version: 10.9.2(@types/node@20.17.8)(typescript@5.7.2)
tslib: tslib:
specifier: ^2.8.1 specifier: ^2.8.1
version: 2.8.1 version: 2.8.1
@ -341,10 +338,10 @@ importers:
version: 5.7.2 version: 5.7.2
vite: vite:
specifier: ^5.4.11 specifier: ^5.4.11
version: 5.4.11(@types/node@20.17.7) version: 5.4.11(@types/node@20.17.8)
vitest: vitest:
specifier: ^1.6.0 specifier: ^1.6.0
version: 1.6.0(@types/node@20.17.7) version: 1.6.0(@types/node@20.17.8)
zod: zod:
specifier: ^3.23.8 specifier: ^3.23.8
version: 3.23.8 version: 3.23.8
@ -2211,6 +2208,10 @@ packages:
'@paralleldrive/cuid2@2.2.2': '@paralleldrive/cuid2@2.2.2':
resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==} resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==}
'@phc/format@1.0.0':
resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==}
engines: {node: '>=10'}
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'} engines: {node: '>=14'}
@ -2347,8 +2348,8 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@scalar/hono-api-reference@0.5.161': '@scalar/hono-api-reference@0.5.162':
resolution: {integrity: sha512-n30VVZrl9z/IcRbyNZZLGx4RMUUH1Zk12ryR1zPG/tdkzNG7ODP6D+rhasn5cSaCg/0BvdCcHEaANEwVJUEjqw==} resolution: {integrity: sha512-WoC6lLXLYSB6OxDuybtYe+5/EdU0M33DPsknjRsOImp+L6Qn64OZvKwyf7033rEHWvqqpwvEB9r+2zZ4F6KhoQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
hono: ^4.0.0 hono: ^4.0.0
@ -2357,8 +2358,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.21': '@scalar/types@0.0.22':
resolution: {integrity: sha512-HR8KeV5zjdVDQdfs7ONhpIRbAzrMS1KIu2sbNgVXtFSaj+1/6WN5gM/yY1DKoaC3oyvpqwd/HyL9rLTqrOkrRw==} resolution: {integrity: sha512-+S1flivP58p2uiHM4dU5ZaAb20wbVcP0nV39KWoVjijvHDx1HWtAGg+PaDXRCRj2zM4QzBeg4olaso20Tm26fQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
'@sideway/address@4.1.5': '@sideway/address@4.1.5':
@ -2400,8 +2401,8 @@ packages:
svelte: ^4.0.0 || ^5.0.0-next.0 svelte: ^4.0.0 || ^5.0.0-next.0
vite: '>= 5.0.0' vite: '>= 5.0.0'
'@sveltejs/kit@2.8.2': '@sveltejs/kit@2.8.5':
resolution: {integrity: sha512-c9My0AnojYtaa96XDAcxcMUdMd3iIhWfrj6BLNtOFz55lMtA/Jima54ZLcYcvfMqei3c86fGRXYa2aIHO+vzFg==} resolution: {integrity: sha512-5ry1jPd4r9knsphDK2eTYUFPhFZMqF0PHFfa8MdMQCqWaKwLSXdFMU/Vevih1I7C1/VNB5MvTuFl1kXu5vx8UA==}
engines: {node: '>=18.13'} engines: {node: '>=18.13'}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@ -2466,8 +2467,8 @@ packages:
'@types/jsonwebtoken@9.0.7': '@types/jsonwebtoken@9.0.7':
resolution: {integrity: sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==} resolution: {integrity: sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==}
'@types/node@20.17.7': '@types/node@20.17.8':
resolution: {integrity: sha512-sZXXnpBFMKbao30dUAvzKbdwA2JM1fwUtVEq/kxKuPI5mMwZiRElCpTXb0Biq/LMEVpXDZL5G5V0RPnxKeyaYg==} resolution: {integrity: sha512-ahz2g6/oqbKalW9sPv6L2iRbhLnojxjYWspAqhjvqSWBgGebEJT5GvRmk0QXPj3sbC6rU0GTQjPLQkmR8CObvA==}
'@types/pg@8.11.10': '@types/pg@8.11.10':
resolution: {integrity: sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg==} resolution: {integrity: sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg==}
@ -2564,8 +2565,8 @@ packages:
'@ungap/structured-clone@1.2.0': '@ungap/structured-clone@1.2.0':
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
'@unhead/schema@1.11.11': '@unhead/schema@1.11.13':
resolution: {integrity: sha512-xSGsWHPBYcMV/ckQeImbrVu6ddeRnrdDCgXUKv3xIjGBY+ob/96V80lGX8FKWh8GwdFSwhblISObKlDAt5K9ZQ==} resolution: {integrity: sha512-fIpQx6GCpl99l4qJXsPqkXxO7suMccuLADbhaMSkeXnVEi4ZIle+l+Ri0z+GHAEpJj17FMaQdO5n9FMSOMUxkw==}
'@vercel/nft@0.27.4': '@vercel/nft@0.27.4':
resolution: {integrity: sha512-Rioz3LJkEKicKCi9BSyc1RXZ5R6GmXosFMeBSThh6msWSOiArKhb7c75MiWwZEgPL7x0/l3TAfH/l0cxKNuUFA==} resolution: {integrity: sha512-Rioz3LJkEKicKCi9BSyc1RXZ5R6GmXosFMeBSThh6msWSOiArKhb7c75MiWwZEgPL7x0/l3TAfH/l0cxKNuUFA==}
@ -2680,8 +2681,8 @@ packages:
aproba@2.0.0: aproba@2.0.0:
resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==}
arctic@1.9.2: arctic@2.3.0:
resolution: {integrity: sha512-VTnGpYx+ypboJdNrWnK17WeD7zN/xSCHnpecd5QYsBfVZde/5i+7DJ1wrf/ioSDMiEjagXmyNWAE3V2C9f1hNg==} resolution: {integrity: sha512-ImueY1iKm044nMVxQGsLvzSFLrLsqCIpsvohZprK2l8o3ypXjoSKiMBlxBBdoFpAG0iC78cJ6J/vyLpvdLQlkw==}
are-we-there-yet@2.0.0: are-we-there-yet@2.0.0:
resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==}
@ -2694,6 +2695,10 @@ packages:
arg@5.0.2: arg@5.0.2:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
argon2@0.41.1:
resolution: {integrity: sha512-dqCW8kJXke8Ik+McUcMDltrbuAWETPyU6iq+4AhxqKphWi7pChB/Zgd/Tp/o8xRLbg8ksMj46F/vph9wnxpTzQ==}
engines: {node: '>=16.17.0'}
argparse@2.0.1: argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
@ -2860,8 +2865,8 @@ packages:
class-validator@0.14.1: class-validator@0.14.1:
resolution: {integrity: sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==} resolution: {integrity: sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==}
class-variance-authority@0.7.0: class-variance-authority@0.7.1:
resolution: {integrity: sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==} resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
classnames@2.5.1: classnames@2.5.1:
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
@ -2876,10 +2881,6 @@ packages:
cliui@6.0.0: cliui@6.0.0:
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
clsx@2.0.0:
resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==}
engines: {node: '>=6'}
clsx@2.1.1: clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -3662,8 +3663,8 @@ packages:
hono: ^4.6.10 hono: ^4.6.10
zod: ^3.21.4 zod: ^3.21.4
hono@4.6.11: hono@4.6.12:
resolution: {integrity: sha512-f0LwJQFKdUUrCUAVowxSvNCjyzI7ZLt8XWYU/EApyeq5FfOvHFarBaE5rjU9HTNFk4RI0FkdB2edb3p/7xZjzQ==} resolution: {integrity: sha512-eHtf4kSDNw6VVrdbd5IQi16r22m3s7mWPLd7xOMhg1a/Yyb1A0qpUFq8xYMX4FMuDe1nTKeMX5rTx7Nmw+a+Ag==}
engines: {node: '>=16.9.0'} engines: {node: '>=16.9.0'}
hookable@5.5.3: hookable@5.5.3:
@ -4091,13 +4092,13 @@ packages:
mz@2.7.0: mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
nanoid@3.3.7: nanoid@3.3.8:
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true hasBin: true
nanoid@5.0.8: nanoid@5.0.9:
resolution: {integrity: sha512-TcJPw+9RV9dibz1hHUzlLVy8N4X9TnwirAjrU08Juo6BNKggzVfP2ZJ/3ZUSq15Xl5i85i+Z89XBO90pB2PghQ==} resolution: {integrity: sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==}
engines: {node: ^18 || >=20} engines: {node: ^18 || >=20}
hasBin: true hasBin: true
@ -4114,6 +4115,10 @@ packages:
node-abort-controller@3.1.1: node-abort-controller@3.1.1:
resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==}
node-addon-api@8.2.2:
resolution: {integrity: sha512-9emqXAKhVoNrQ792nLI/wpzPpJ/bj/YXxW0CvAau1+RdGBcCRF1Dmz7719zgVsQNrzHl9Tzn3ImZ4qWFarWL0A==}
engines: {node: ^18 || ^20 || >= 21}
node-fetch@2.7.0: node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0} engines: {node: 4.x || >=6.0.0}
@ -4210,9 +4215,6 @@ packages:
oslo@1.2.0: oslo@1.2.0:
resolution: {integrity: sha512-OoFX6rDsNcOQVAD2gQD/z03u4vEjWZLzJtwkmgfRF+KpQUXwdgEXErD7zNhyowmHwHefP+PM9Pw13pgpHMRlzw==} resolution: {integrity: sha512-OoFX6rDsNcOQVAD2gQD/z03u4vEjWZLzJtwkmgfRF+KpQUXwdgEXErD7zNhyowmHwHefP+PM9Pw13pgpHMRlzw==}
oslo@1.2.1:
resolution: {integrity: sha512-HfIhB5ruTdQv0XX2XlncWQiJ5SIHZ7NHZhVyHth0CSZ/xzge00etRyYy/3wp/Dsu+PkxMC+6+B2lS/GcKoewkA==}
p-limit@2.3.0: p-limit@2.3.0:
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -4657,8 +4659,8 @@ packages:
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
prettier@3.3.3: prettier@3.4.1:
resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} resolution: {integrity: sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg==}
engines: {node: '>=14'} engines: {node: '>=14'}
hasBin: true hasBin: true
@ -4948,11 +4950,11 @@ packages:
std-env@3.7.0: std-env@3.7.0:
resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==}
stoker@1.3.0: stoker@1.4.2:
resolution: {integrity: sha512-ywRokjO8jKb65z6qJVJbzilQJzcoly8/bwyodp6sZC0ZQmn/zfDCOkjcw6hhNKWE8eSVGanZUIXEPab5MTxnmA==} resolution: {integrity: sha512-zna86ZzC3fnMOIkuO+1vRMfcRw7SpC/7yafRb0u8DwDVig2pPh6POVnGB7t2A5t/rMvyr7hE7tjXTPvW8bhJKg==}
peerDependencies: peerDependencies:
'@asteasolutions/zod-to-openapi': ^7.0.0 '@asteasolutions/zod-to-openapi': ^7.0.0
'@hono/zod-openapi': ^0.16.0 '@hono/zod-openapi': '>=0.16.0'
hono: ^4.0.0 hono: ^4.0.0
openapi3-ts: ^4.4.0 openapi3-ts: ^4.4.0
peerDependenciesMeta: peerDependenciesMeta:
@ -5263,9 +5265,6 @@ packages:
'@swc/wasm': '@swc/wasm':
optional: true optional: true
tslib@1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
tslib@2.4.0: tslib@2.4.0:
resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==}
@ -5280,10 +5279,6 @@ packages:
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
hasBin: true hasBin: true
tsyringe@4.8.0:
resolution: {integrity: sha512-YB1FG+axdxADa3ncEtRnQCFq/M0lALGLxSZeVNbTU8NqhOVc51nnv2CISTcvc1kyv6EGPtXVr0v6lWeDxiijOA==}
engines: {node: '>= 6.0.0'}
type-check@0.4.0: type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@ -6328,25 +6323,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.11)': '@hono/swagger-ui@0.4.1(hono@4.6.12)':
dependencies: dependencies:
hono: 4.6.11 hono: 4.6.12
'@hono/zod-openapi@0.15.3(hono@4.6.11)(zod@3.23.8)': '@hono/zod-openapi@0.15.3(hono@4.6.12)(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.11)(zod@3.23.8) '@hono/zod-validator': 0.2.2(hono@4.6.12)(zod@3.23.8)
hono: 4.6.11 hono: 4.6.12
zod: 3.23.8 zod: 3.23.8
'@hono/zod-validator@0.2.2(hono@4.6.11)(zod@3.23.8)': '@hono/zod-validator@0.2.2(hono@4.6.12)(zod@3.23.8)':
dependencies: dependencies:
hono: 4.6.11 hono: 4.6.12
zod: 3.23.8 zod: 3.23.8
'@hono/zod-validator@0.4.1(hono@4.6.11)(zod@3.23.8)': '@hono/zod-validator@0.4.1(hono@4.6.12)(zod@3.23.8)':
dependencies: dependencies:
hono: 4.6.11 hono: 4.6.12
zod: 3.23.8 zod: 3.23.8
'@humanwhocodes/config-array@0.13.0': '@humanwhocodes/config-array@0.13.0':
@ -6490,12 +6485,12 @@ snapshots:
- babel-plugin-macros - babel-plugin-macros
- debug - debug
'@inlang/paraglide-sveltekit@0.11.5(@sveltejs/kit@2.8.2(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))': '@inlang/paraglide-sveltekit@0.11.5(@sveltejs/kit@2.8.5(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))':
dependencies: dependencies:
'@inlang/paraglide-js': 1.11.3 '@inlang/paraglide-js': 1.11.3
'@inlang/paraglide-vite': 1.2.76 '@inlang/paraglide-vite': 1.2.76
'@lix-js/client': 2.2.1 '@lix-js/client': 2.2.1
'@sveltejs/kit': 2.8.2(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)) '@sveltejs/kit': 2.8.5(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8))
commander: 12.1.0 commander: 12.1.0
dedent: 1.5.1 dedent: 1.5.1
devalue: 4.3.3 devalue: 4.3.3
@ -6671,7 +6666,7 @@ snapshots:
'@internationalized/date': 3.6.0 '@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.8 nanoid: 5.0.9
svelte: 5.0.0-next.175 svelte: 5.0.0-next.175
'@melt-ui/svelte@0.83.0(svelte@5.0.0-next.175)': '@melt-ui/svelte@0.83.0(svelte@5.0.0-next.175)':
@ -6681,7 +6676,7 @@ snapshots:
'@internationalized/date': 3.6.0 '@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.8 nanoid: 5.0.9
svelte: 5.0.0-next.175 svelte: 5.0.0-next.175
'@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3':
@ -7128,6 +7123,8 @@ snapshots:
dependencies: dependencies:
'@noble/hashes': 1.5.0 '@noble/hashes': 1.5.0
'@phc/format@1.0.0': {}
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
optional: true optional: true
@ -7229,17 +7226,17 @@ 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.161(hono@4.6.11)': '@scalar/hono-api-reference@0.5.162(hono@4.6.12)':
dependencies: dependencies:
'@scalar/types': 0.0.21 '@scalar/types': 0.0.22
hono: 4.6.11 hono: 4.6.12
'@scalar/openapi-types@0.1.5': {} '@scalar/openapi-types@0.1.5': {}
'@scalar/types@0.0.21': '@scalar/types@0.0.22':
dependencies: dependencies:
'@scalar/openapi-types': 0.1.5 '@scalar/openapi-types': 0.1.5
'@unhead/schema': 1.11.11 '@unhead/schema': 1.11.13
'@sideway/address@4.1.5': '@sideway/address@4.1.5':
dependencies: dependencies:
@ -7259,41 +7256,41 @@ snapshots:
'@sinclair/typebox@0.32.35': '@sinclair/typebox@0.32.35':
optional: true optional: true
'@sveltejs/adapter-auto@3.3.1(@sveltejs/kit@2.8.2(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))': '@sveltejs/adapter-auto@3.3.1(@sveltejs/kit@2.8.5(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))':
dependencies: dependencies:
'@sveltejs/kit': 2.8.2(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)) '@sveltejs/kit': 2.8.5(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8))
import-meta-resolve: 4.1.0 import-meta-resolve: 4.1.0
'@sveltejs/adapter-node@5.2.9(@sveltejs/kit@2.8.2(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))': '@sveltejs/adapter-node@5.2.9(@sveltejs/kit@2.8.5(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))':
dependencies: dependencies:
'@rollup/plugin-commonjs': 28.0.1(rollup@4.24.0) '@rollup/plugin-commonjs': 28.0.1(rollup@4.24.0)
'@rollup/plugin-json': 6.1.0(rollup@4.24.0) '@rollup/plugin-json': 6.1.0(rollup@4.24.0)
'@rollup/plugin-node-resolve': 15.3.0(rollup@4.24.0) '@rollup/plugin-node-resolve': 15.3.0(rollup@4.24.0)
'@sveltejs/kit': 2.8.2(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)) '@sveltejs/kit': 2.8.5(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8))
rollup: 4.24.0 rollup: 4.24.0
'@sveltejs/adapter-vercel@5.4.8(@sveltejs/kit@2.8.2(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))': '@sveltejs/adapter-vercel@5.4.8(@sveltejs/kit@2.8.5(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))':
dependencies: dependencies:
'@sveltejs/kit': 2.8.2(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)) '@sveltejs/kit': 2.8.5(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8))
'@vercel/nft': 0.27.4 '@vercel/nft': 0.27.4
esbuild: 0.21.5 esbuild: 0.21.5
transitivePeerDependencies: transitivePeerDependencies:
- encoding - encoding
- supports-color - supports-color
'@sveltejs/enhanced-img@0.3.10(rollup@4.24.0)(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7))': '@sveltejs/enhanced-img@0.3.10(rollup@4.24.0)(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8))':
dependencies: dependencies:
magic-string: 0.30.11 magic-string: 0.30.11
svelte: 5.0.0-next.175 svelte: 5.0.0-next.175
svelte-parse-markup: 0.1.5(svelte@5.0.0-next.175) svelte-parse-markup: 0.1.5(svelte@5.0.0-next.175)
vite: 5.4.11(@types/node@20.17.7) vite: 5.4.11(@types/node@20.17.8)
vite-imagetools: 7.0.4(rollup@4.24.0) vite-imagetools: 7.0.4(rollup@4.24.0)
transitivePeerDependencies: transitivePeerDependencies:
- rollup - rollup
'@sveltejs/kit@2.8.2(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7))': '@sveltejs/kit@2.8.5(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8))':
dependencies: dependencies:
'@sveltejs/vite-plugin-svelte': 4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)) '@sveltejs/vite-plugin-svelte': 4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8))
'@types/cookie': 0.6.0 '@types/cookie': 0.6.0
cookie: 0.6.0 cookie: 0.6.0
devalue: 5.1.1 devalue: 5.1.1
@ -7307,27 +7304,27 @@ snapshots:
sirv: 3.0.0 sirv: 3.0.0
svelte: 5.0.0-next.175 svelte: 5.0.0-next.175
tiny-glob: 0.2.9 tiny-glob: 0.2.9
vite: 5.4.11(@types/node@20.17.7) vite: 5.4.11(@types/node@20.17.8)
'@sveltejs/vite-plugin-svelte-inspector@3.0.0-next.3(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7))': '@sveltejs/vite-plugin-svelte-inspector@3.0.0-next.3(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8))':
dependencies: dependencies:
'@sveltejs/vite-plugin-svelte': 4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)) '@sveltejs/vite-plugin-svelte': 4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8))
debug: 4.3.7 debug: 4.3.7
svelte: 5.0.0-next.175 svelte: 5.0.0-next.175
vite: 5.4.11(@types/node@20.17.7) vite: 5.4.11(@types/node@20.17.8)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7))': '@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8))':
dependencies: dependencies:
'@sveltejs/vite-plugin-svelte-inspector': 3.0.0-next.3(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)) '@sveltejs/vite-plugin-svelte-inspector': 3.0.0-next.3(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8))
debug: 4.3.7 debug: 4.3.7
deepmerge: 4.3.1 deepmerge: 4.3.1
kleur: 4.1.5 kleur: 4.1.5
magic-string: 0.30.11 magic-string: 0.30.11
svelte: 5.0.0-next.175 svelte: 5.0.0-next.175
vite: 5.4.11(@types/node@20.17.7) vite: 5.4.11(@types/node@20.17.8)
vitefu: 1.0.2(vite@5.4.11(@types/node@20.17.7)) vitefu: 1.0.2(vite@5.4.11(@types/node@20.17.8))
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -7368,21 +7365,21 @@ snapshots:
'@types/jsonwebtoken@9.0.7': '@types/jsonwebtoken@9.0.7':
dependencies: dependencies:
'@types/node': 20.17.7 '@types/node': 20.17.8
'@types/node@20.17.7': '@types/node@20.17.8':
dependencies: dependencies:
undici-types: 6.19.8 undici-types: 6.19.8
'@types/pg@8.11.10': '@types/pg@8.11.10':
dependencies: dependencies:
'@types/node': 20.17.7 '@types/node': 20.17.8
pg-protocol: 1.7.0 pg-protocol: 1.7.0
pg-types: 4.0.2 pg-types: 4.0.2
'@types/pg@8.11.6': '@types/pg@8.11.6':
dependencies: dependencies:
'@types/node': 20.17.7 '@types/node': 20.17.8
pg-protocol: 1.7.0 pg-protocol: 1.7.0
pg-types: 4.0.2 pg-types: 4.0.2
@ -7390,7 +7387,7 @@ snapshots:
'@types/qrcode@1.5.5': '@types/qrcode@1.5.5':
dependencies: dependencies:
'@types/node': 20.17.7 '@types/node': 20.17.8
'@types/resolve@1.20.2': {} '@types/resolve@1.20.2': {}
@ -7494,7 +7491,7 @@ snapshots:
'@ungap/structured-clone@1.2.0': {} '@ungap/structured-clone@1.2.0': {}
'@unhead/schema@1.11.11': '@unhead/schema@1.11.13':
dependencies: dependencies:
hookable: 5.5.3 hookable: 5.5.3
zhead: 2.2.4 zhead: 2.2.4
@ -7633,9 +7630,11 @@ snapshots:
aproba@2.0.0: {} aproba@2.0.0: {}
arctic@1.9.2: arctic@2.3.0:
dependencies: dependencies:
oslo: 1.2.0 '@oslojs/crypto': 1.0.1
'@oslojs/encoding': 1.1.0
'@oslojs/jwt': 0.2.0
are-we-there-yet@2.0.0: are-we-there-yet@2.0.0:
dependencies: dependencies:
@ -7646,6 +7645,12 @@ snapshots:
arg@5.0.2: {} arg@5.0.2: {}
argon2@0.41.1:
dependencies:
'@phc/format': 1.0.0
node-addon-api: 8.2.2
node-gyp-build: 4.8.2
argparse@2.0.1: {} argparse@2.0.1: {}
aria-query@5.3.2: {} aria-query@5.3.2: {}
@ -7706,7 +7711,7 @@ snapshots:
dependencies: dependencies:
'@internationalized/date': 3.6.0 '@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.8 nanoid: 5.0.9
svelte: 5.0.0-next.175 svelte: 5.0.0-next.175
boardgamegeekclient@1.9.1: boardgamegeekclient@1.9.1:
@ -7844,9 +7849,9 @@ snapshots:
validator: 13.12.0 validator: 13.12.0
optional: true optional: true
class-variance-authority@0.7.0: class-variance-authority@0.7.1:
dependencies: dependencies:
clsx: 2.0.0 clsx: 2.1.1
classnames@2.5.1: {} classnames@2.5.1: {}
@ -7860,8 +7865,6 @@ snapshots:
strip-ansi: 6.0.1 strip-ansi: 6.0.1
wrap-ansi: 6.2.0 wrap-ansi: 6.2.0
clsx@2.0.0: {}
clsx@2.1.1: {} clsx@2.1.1: {}
cluster-key-slot@1.1.2: {} cluster-key-slot@1.1.2: {}
@ -7977,8 +7980,7 @@ snapshots:
dateformat@4.6.3: {} dateformat@4.6.3: {}
dayjs@1.11.13: dayjs@1.11.13: {}
optional: true
debug@2.6.9: debug@2.6.9:
dependencies: dependencies:
@ -8500,11 +8502,11 @@ snapshots:
combined-stream: 1.0.8 combined-stream: 1.0.8
mime-types: 2.1.35 mime-types: 2.1.35
formsnap@1.0.1(svelte@5.0.0-next.175)(sveltekit-superforms@2.20.1(@sveltejs/kit@2.8.2(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(@types/json-schema@7.0.15)(svelte@5.0.0-next.175)(typescript@5.7.2)): formsnap@1.0.1(svelte@5.0.0-next.175)(sveltekit-superforms@2.20.1(@sveltejs/kit@2.8.5(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(@types/json-schema@7.0.15)(svelte@5.0.0-next.175)(typescript@5.7.2)):
dependencies: dependencies:
nanoid: 5.0.8 nanoid: 5.0.9
svelte: 5.0.0-next.175 svelte: 5.0.0-next.175
sveltekit-superforms: 2.20.1(@sveltejs/kit@2.8.2(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(@types/json-schema@7.0.15)(svelte@5.0.0-next.175)(typescript@5.7.2) sveltekit-superforms: 2.20.1(@sveltejs/kit@2.8.5(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(@types/json-schema@7.0.15)(svelte@5.0.0-next.175)(typescript@5.7.2)
forwarded@0.2.0: {} forwarded@0.2.0: {}
@ -8639,24 +8641,24 @@ snapshots:
help-me@5.0.0: {} help-me@5.0.0: {}
hono-pino@0.7.0(hono@4.6.11)(pino@9.5.0): hono-pino@0.7.0(hono@4.6.12)(pino@9.5.0):
dependencies: dependencies:
defu: 6.1.4 defu: 6.1.4
hono: 4.6.11 hono: 4.6.12
pino: 9.5.0 pino: 9.5.0
hono-rate-limiter@0.4.0(hono@4.6.11): hono-rate-limiter@0.4.0(hono@4.6.12):
dependencies: dependencies:
hono: 4.6.11 hono: 4.6.12
hono-zod-openapi@0.5.0(hono@4.6.11)(zod@3.23.8): hono-zod-openapi@0.5.0(hono@4.6.12)(zod@3.23.8):
dependencies: dependencies:
'@hono/zod-validator': 0.4.1(hono@4.6.11)(zod@3.23.8) '@hono/zod-validator': 0.4.1(hono@4.6.12)(zod@3.23.8)
hono: 4.6.11 hono: 4.6.12
zod: 3.23.8 zod: 3.23.8
zod-openapi: 4.0.0(zod@3.23.8) zod-openapi: 4.0.0(zod@3.23.8)
hono@4.6.11: {} hono@4.6.12: {}
hookable@5.5.3: {} hookable@5.5.3: {}
@ -9056,9 +9058,9 @@ snapshots:
object-assign: 4.1.1 object-assign: 4.1.1
thenify-all: 1.6.0 thenify-all: 1.6.0
nanoid@3.3.7: {} nanoid@3.3.8: {}
nanoid@5.0.8: {} nanoid@5.0.9: {}
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
@ -9068,6 +9070,8 @@ snapshots:
node-abort-controller@3.1.1: {} node-abort-controller@3.1.1: {}
node-addon-api@8.2.2: {}
node-fetch@2.7.0: node-fetch@2.7.0:
dependencies: dependencies:
whatwg-url: 5.0.0 whatwg-url: 5.0.0
@ -9160,11 +9164,6 @@ snapshots:
'@node-rs/argon2': 1.7.0 '@node-rs/argon2': 1.7.0
'@node-rs/bcrypt': 1.9.0 '@node-rs/bcrypt': 1.9.0
oslo@1.2.1:
dependencies:
'@node-rs/argon2': 1.7.0
'@node-rs/bcrypt': 1.9.0
p-limit@2.3.0: p-limit@2.3.0:
dependencies: dependencies:
p-try: 2.2.0 p-try: 2.2.0
@ -9460,13 +9459,13 @@ snapshots:
'@csstools/utilities': 1.0.0(postcss@8.4.49) '@csstools/utilities': 1.0.0(postcss@8.4.49)
postcss: 8.4.49 postcss: 8.4.49
postcss-load-config@4.0.2(postcss@8.4.49)(ts-node@10.9.2(@types/node@20.17.7)(typescript@5.7.2)): postcss-load-config@4.0.2(postcss@8.4.49)(ts-node@10.9.2(@types/node@20.17.8)(typescript@5.7.2)):
dependencies: dependencies:
lilconfig: 3.1.2 lilconfig: 3.1.2
yaml: 2.5.1 yaml: 2.5.1
optionalDependencies: optionalDependencies:
postcss: 8.4.49 postcss: 8.4.49
ts-node: 10.9.2(@types/node@20.17.7)(typescript@5.7.2) ts-node: 10.9.2(@types/node@20.17.8)(typescript@5.7.2)
postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.49)(tsx@4.19.2): postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.49)(tsx@4.19.2):
dependencies: dependencies:
@ -9600,7 +9599,7 @@ snapshots:
postcss@8.4.49: postcss@8.4.49:
dependencies: dependencies:
nanoid: 3.3.7 nanoid: 3.3.8
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.1 source-map-js: 1.2.1
@ -9637,12 +9636,12 @@ snapshots:
prelude-ls@1.2.1: {} prelude-ls@1.2.1: {}
prettier-plugin-svelte@3.3.2(prettier@3.3.3)(svelte@5.0.0-next.175): prettier-plugin-svelte@3.3.2(prettier@3.4.1)(svelte@5.0.0-next.175):
dependencies: dependencies:
prettier: 3.3.3 prettier: 3.4.1
svelte: 5.0.0-next.175 svelte: 5.0.0-next.175
prettier@3.3.3: {} prettier@3.4.1: {}
pretty-format@29.7.0: pretty-format@29.7.0:
dependencies: dependencies:
@ -9965,13 +9964,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.11)(zod@3.23.8))(hono@4.6.11)(openapi3-ts@4.4.0): stoker@1.4.2(@asteasolutions/zod-to-openapi@7.1.2(zod@3.23.8))(@hono/zod-openapi@0.15.3(hono@4.6.12)(zod@3.23.8))(hono@4.6.12)(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.11 hono: 4.6.12
openapi3-ts: 4.4.0 openapi3-ts: 4.4.0
optionalDependencies: optionalDependencies:
'@hono/zod-openapi': 0.15.3(hono@4.6.11)(zod@3.23.8) '@hono/zod-openapi': 0.15.3(hono@4.6.12)(zod@3.23.8)
string-width@4.2.3: string-width@4.2.3:
dependencies: dependencies:
@ -10146,14 +10145,14 @@ snapshots:
magic-string: 0.30.11 magic-string: 0.30.11
zimmerframe: 1.1.2 zimmerframe: 1.1.2
sveltekit-flash-message@2.4.4(@sveltejs/kit@2.8.2(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(svelte@5.0.0-next.175): sveltekit-flash-message@2.4.4(@sveltejs/kit@2.8.5(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(svelte@5.0.0-next.175):
dependencies: dependencies:
'@sveltejs/kit': 2.8.2(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)) '@sveltejs/kit': 2.8.5(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8))
svelte: 5.0.0-next.175 svelte: 5.0.0-next.175
sveltekit-superforms@2.20.1(@sveltejs/kit@2.8.2(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(@types/json-schema@7.0.15)(svelte@5.0.0-next.175)(typescript@5.7.2): sveltekit-superforms@2.20.1(@sveltejs/kit@2.8.5(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(@types/json-schema@7.0.15)(svelte@5.0.0-next.175)(typescript@5.7.2):
dependencies: dependencies:
'@sveltejs/kit': 2.8.2(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.7)) '@sveltejs/kit': 2.8.5(@sveltejs/vite-plugin-svelte@4.0.0-next.7(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8)))(svelte@5.0.0-next.175)(vite@5.4.11(@types/node@20.17.8))
devalue: 5.1.1 devalue: 5.1.1
just-clone: 6.2.0 just-clone: 6.2.0
memoize-weak: 1.0.2 memoize-weak: 1.0.2
@ -10184,16 +10183,16 @@ snapshots:
tailwind-merge@2.5.5: {} tailwind-merge@2.5.5: {}
tailwind-variants@0.2.1(tailwindcss@3.4.15(ts-node@10.9.2(@types/node@20.17.7)(typescript@5.7.2))): tailwind-variants@0.2.1(tailwindcss@3.4.15(ts-node@10.9.2(@types/node@20.17.8)(typescript@5.7.2))):
dependencies: dependencies:
tailwind-merge: 2.5.5 tailwind-merge: 2.5.5
tailwindcss: 3.4.15(ts-node@10.9.2(@types/node@20.17.7)(typescript@5.7.2)) tailwindcss: 3.4.15(ts-node@10.9.2(@types/node@20.17.8)(typescript@5.7.2))
tailwindcss-animate@1.0.7(tailwindcss@3.4.15(ts-node@10.9.2(@types/node@20.17.7)(typescript@5.7.2))): tailwindcss-animate@1.0.7(tailwindcss@3.4.15(ts-node@10.9.2(@types/node@20.17.8)(typescript@5.7.2))):
dependencies: dependencies:
tailwindcss: 3.4.15(ts-node@10.9.2(@types/node@20.17.7)(typescript@5.7.2)) tailwindcss: 3.4.15(ts-node@10.9.2(@types/node@20.17.8)(typescript@5.7.2))
tailwindcss@3.4.15(ts-node@10.9.2(@types/node@20.17.7)(typescript@5.7.2)): tailwindcss@3.4.15(ts-node@10.9.2(@types/node@20.17.8)(typescript@5.7.2)):
dependencies: dependencies:
'@alloc/quick-lru': 5.2.0 '@alloc/quick-lru': 5.2.0
arg: 5.0.2 arg: 5.0.2
@ -10212,7 +10211,7 @@ snapshots:
postcss: 8.4.49 postcss: 8.4.49
postcss-import: 15.1.0(postcss@8.4.49) postcss-import: 15.1.0(postcss@8.4.49)
postcss-js: 4.0.1(postcss@8.4.49) postcss-js: 4.0.1(postcss@8.4.49)
postcss-load-config: 4.0.2(postcss@8.4.49)(ts-node@10.9.2(@types/node@20.17.7)(typescript@5.7.2)) postcss-load-config: 4.0.2(postcss@8.4.49)(ts-node@10.9.2(@types/node@20.17.8)(typescript@5.7.2))
postcss-nested: 6.2.0(postcss@8.4.49) postcss-nested: 6.2.0(postcss@8.4.49)
postcss-selector-parser: 6.1.2 postcss-selector-parser: 6.1.2
resolve: 1.22.8 resolve: 1.22.8
@ -10283,14 +10282,14 @@ snapshots:
ts-interface-checker@0.1.13: {} ts-interface-checker@0.1.13: {}
ts-node@10.9.2(@types/node@20.17.7)(typescript@5.7.2): ts-node@10.9.2(@types/node@20.17.8)(typescript@5.7.2):
dependencies: dependencies:
'@cspotcode/source-map-support': 0.8.1 '@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.11 '@tsconfig/node10': 1.0.11
'@tsconfig/node12': 1.0.11 '@tsconfig/node12': 1.0.11
'@tsconfig/node14': 1.0.3 '@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.4 '@tsconfig/node16': 1.0.4
'@types/node': 20.17.7 '@types/node': 20.17.8
acorn: 8.12.1 acorn: 8.12.1
acorn-walk: 8.3.4 acorn-walk: 8.3.4
arg: 4.1.3 arg: 4.1.3
@ -10301,8 +10300,6 @@ snapshots:
v8-compile-cache-lib: 3.0.1 v8-compile-cache-lib: 3.0.1
yn: 3.1.1 yn: 3.1.1
tslib@1.14.1: {}
tslib@2.4.0: tslib@2.4.0:
optional: true optional: true
@ -10317,10 +10314,6 @@ snapshots:
optionalDependencies: optionalDependencies:
fsevents: 2.3.3 fsevents: 2.3.3
tsyringe@4.8.0:
dependencies:
tslib: 1.14.1
type-check@0.4.0: type-check@0.4.0:
dependencies: dependencies:
prelude-ls: 1.2.1 prelude-ls: 1.2.1
@ -10403,13 +10396,13 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- rollup - rollup
vite-node@1.6.0(@types/node@20.17.7): vite-node@1.6.0(@types/node@20.17.8):
dependencies: dependencies:
cac: 6.7.14 cac: 6.7.14
debug: 4.3.7 debug: 4.3.7
pathe: 1.1.2 pathe: 1.1.2
picocolors: 1.1.0 picocolors: 1.1.0
vite: 5.4.11(@types/node@20.17.7) vite: 5.4.11(@types/node@20.17.8)
transitivePeerDependencies: transitivePeerDependencies:
- '@types/node' - '@types/node'
- less - less
@ -10421,20 +10414,20 @@ snapshots:
- supports-color - supports-color
- terser - terser
vite@5.4.11(@types/node@20.17.7): vite@5.4.11(@types/node@20.17.8):
dependencies: dependencies:
esbuild: 0.21.5 esbuild: 0.21.5
postcss: 8.4.49 postcss: 8.4.49
rollup: 4.24.0 rollup: 4.24.0
optionalDependencies: optionalDependencies:
'@types/node': 20.17.7 '@types/node': 20.17.8
fsevents: 2.3.3 fsevents: 2.3.3
vitefu@1.0.2(vite@5.4.11(@types/node@20.17.7)): vitefu@1.0.2(vite@5.4.11(@types/node@20.17.8)):
optionalDependencies: optionalDependencies:
vite: 5.4.11(@types/node@20.17.7) vite: 5.4.11(@types/node@20.17.8)
vitest@1.6.0(@types/node@20.17.7): vitest@1.6.0(@types/node@20.17.8):
dependencies: dependencies:
'@vitest/expect': 1.6.0 '@vitest/expect': 1.6.0
'@vitest/runner': 1.6.0 '@vitest/runner': 1.6.0
@ -10453,11 +10446,11 @@ snapshots:
strip-literal: 2.1.0 strip-literal: 2.1.0
tinybench: 2.9.0 tinybench: 2.9.0
tinypool: 0.8.4 tinypool: 0.8.4
vite: 5.4.11(@types/node@20.17.7) vite: 5.4.11(@types/node@20.17.8)
vite-node: 1.6.0(@types/node@20.17.7) vite-node: 1.6.0(@types/node@20.17.8)
why-is-node-running: 2.3.0 why-is-node-running: 2.3.0
optionalDependencies: optionalDependencies:
'@types/node': 20.17.7 '@types/node': 20.17.8
transitivePeerDependencies: transitivePeerDependencies:
- less - less
- lightningcss - lightningcss

2
src/app.d.ts vendored
View file

@ -1,6 +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 { Session } from '$lib/server/api/iam/sessions/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

View file

@ -1,21 +1,20 @@
import "reflect-metadata"; import { i18n } from '$lib/i18n';
import { StatusCodes } from "$lib/constants/status-codes"; import type { ApiRoutes } from '$lib/server/api';
import type { ApiRoutes } from "$lib/server/api"; import { parseApiResponse } from '$lib/utils/api';
import { parseApiResponse } from "$lib/utils/api"; import { StatusCodes } from '$lib/utils/status-codes';
import { type Handle, redirect } from "@sveltejs/kit"; import { type Handle, redirect } from '@sveltejs/kit';
import { sequence } from "@sveltejs/kit/hooks"; import { sequence } from '@sveltejs/kit/hooks';
import { hc } from "hono/client"; import { hc } from 'hono/client';
import { i18n } from "$lib/i18n";
const handleParaglide: Handle = i18n.handle(); const handleParaglide: Handle = i18n.handle();
const apiClient: Handle = async ({ event, resolve }) => { const apiClient: Handle = async ({ event, resolve }) => {
/* ------------------------------ Register api ------------------------------ */ /* ------------------------------ Register api ------------------------------ */
const { api } = hc<ApiRoutes>("/", { const { api } = hc<ApiRoutes>('/', {
fetch: event.fetch, fetch: event.fetch,
headers: { headers: {
"x-forwarded-for": event.url.host.includes("sveltekit-prerender") ? "127.0.0.1" : event.getClientAddress(), 'x-forwarded-for': event.url.host.includes('sveltekit-prerender') ? '127.0.0.1' : event.getClientAddress(),
host: event.request.headers.get("host") || "", host: event.request.headers.get('host') || '',
}, },
}); });
@ -28,7 +27,7 @@ const apiClient: Handle = async ({ event, resolve }) => {
async function getAuthedUserOrThrow() { async function getAuthedUserOrThrow() {
const { data } = await api.me.$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, '/');
} }
return data?.user; return data?.user;
} }

View file

@ -1,357 +0,0 @@
// Taken from https://github.com/prettymuchbryce/http-status-codes/blob/master/src/status-codes.ts
export enum StatusCodes {
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.2.1
*
* This interim response indicates that everything so far is OK and that the client should continue with the request or ignore it if it is already finished.
*/
CONTINUE = 100,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.2.2
*
* This code is sent in response to an Upgrade request header by the client, and indicates the protocol the server is switching too.
*/
SWITCHING_PROTOCOLS = 101,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.1
*
* This code indicates that the server has received and is processing the request, but no response is available yet.
*/
PROCESSING = 102,
/**
* Official Documentation @ https://www.rfc-editor.org/rfc/rfc8297#page-3
*
* This code indicates to the client that the server is likely to send a final response with the header fields included in the informational response.
*/
EARLY_HINTS = 103,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.3.1
*
* The request has succeeded. The meaning of a success varies depending on the HTTP method:
* GET: The resource has been fetched and is transmitted in the message body.
* HEAD: The entity headers are in the message body.
* POST: The resource describing the result of the action is transmitted in the message body.
* TRACE: The message body contains the request message as received by the server
*/
OK = 200,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.3.2
*
* The request has succeeded and a new resource has been created as a result of it. This is typically the response sent after a PUT request.
*/
CREATED = 201,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.3.3
*
* The request has been received but not yet acted upon. It is non-committal, meaning that there is no way in HTTP to later send an asynchronous response indicating the outcome of processing the request. It is intended for cases where another process or server handles the request, or for batch processing.
*/
ACCEPTED = 202,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.3.4
*
* This response code means returned meta-information set is not exact set as available from the origin server, but collected from a local or a third party copy. Except this condition, 200 OK response should be preferred instead of this response.
*/
NON_AUTHORITATIVE_INFORMATION = 203,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.3.5
*
* There is no content to send for this request, but the headers may be useful. The user-agent may update its cached headers for this resource with the new ones.
*/
NO_CONTENT = 204,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.3.6
*
* This response code is sent after accomplishing request to tell user agent reset document view which sent this request.
*/
RESET_CONTENT = 205,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7233#section-4.1
*
* This response code is used because of range header sent by the client to separate download into multiple streams.
*/
PARTIAL_CONTENT = 206,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.2
*
* A Multi-Status response conveys information about multiple resources in situations where multiple status codes might be appropriate.
*/
MULTI_STATUS = 207,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.4.1
*
* The request has more than one possible responses. User-agent or user should choose one of them. There is no standardized way to choose one of the responses.
*/
MULTIPLE_CHOICES = 300,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.4.2
*
* This response code means that URI of requested resource has been changed. Probably, new URI would be given in the response.
*/
MOVED_PERMANENTLY = 301,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.4.3
*
* This response code means that URI of requested resource has been changed temporarily. New changes in the URI might be made in the future. Therefore, this same URI should be used by the client in future requests.
*/
MOVED_TEMPORARILY = 302,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.4.4
*
* Server sent this response to directing client to get requested resource to another URI with an GET request.
*/
SEE_OTHER = 303,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7232#section-4.1
*
* This is used for caching purposes. It is telling to client that response has not been modified. So, client can continue to use same cached version of response.
*/
NOT_MODIFIED = 304,
/**
* @deprecated
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.4.6
*
* Was defined in a previous version of the HTTP specification to indicate that a requested response must be accessed by a proxy. It has been deprecated due to security concerns regarding in-band configuration of a proxy.
*/
USE_PROXY = 305,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.4.7
*
* Server sent this response to directing client to get requested resource to another URI with same method that used prior request. This has the same semantic than the 302 Found HTTP response code, with the exception that the user agent must not change the HTTP method used: if a POST was used in the first request, a POST must be used in the second request.
*/
TEMPORARY_REDIRECT = 307,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7538#section-3
*
* This means that the resource is now permanently located at another URI, specified by the Location: HTTP Response header. This has the same semantics as the 301 Moved Permanently HTTP response code, with the exception that the user agent must not change the HTTP method used: if a POST was used in the first request, a POST must be used in the second request.
*/
PERMANENT_REDIRECT = 308,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.1
*
* This response means that server could not understand the request due to invalid syntax.
*/
BAD_REQUEST = 400,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7235#section-3.1
*
* Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the client must authenticate itself to get the requested response.
*/
UNAUTHORIZED = 401,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.2
*
* This response code is reserved for future use. Initial aim for creating this code was using it for digital payment systems however this is not used currently.
*/
PAYMENT_REQUIRED = 402,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.3
*
* The client does not have access rights to the content, i.e. they are unauthorized, so server is rejecting to give proper response. Unlike 401, the client's identity is known to the server.
*/
FORBIDDEN = 403,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.4
*
* The server can not find requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of 403 to hide the existence of a resource from an unauthorized client. This response code is probably the most famous one due to its frequent occurence on the web.
*/
NOT_FOUND = 404,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.5
*
* The request method is known by the server but has been disabled and cannot be used. For example, an API may forbid DELETE-ing a resource. The two mandatory methods, GET and HEAD, must never be disabled and should not return this error code.
*/
METHOD_NOT_ALLOWED = 405,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.6
*
* This response is sent when the web server, after performing server-driven content negotiation, doesn't find any content following the criteria given by the user agent.
*/
NOT_ACCEPTABLE = 406,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7235#section-3.2
*
* This is similar to 401 but authentication is needed to be done by a proxy.
*/
PROXY_AUTHENTICATION_REQUIRED = 407,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.7
*
* This response is sent on an idle connection by some servers, even without any previous request by the client. It means that the server would like to shut down this unused connection. This response is used much more since some browsers, like Chrome, Firefox 27+, or IE9, use HTTP pre-connection mechanisms to speed up surfing. Also note that some servers merely shut down the connection without sending this message.
*/
REQUEST_TIMEOUT = 408,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.8
*
* This response is sent when a request conflicts with the current state of the server.
*/
CONFLICT = 409,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.9
*
* This response would be sent when the requested content has been permenantly deleted from server, with no forwarding address. Clients are expected to remove their caches and links to the resource. The HTTP specification intends this status code to be used for "limited-time, promotional services". APIs should not feel compelled to indicate resources that have been deleted with this status code.
*/
GONE = 410,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.10
*
* The server rejected the request because the Content-Length header field is not defined and the server requires it.
*/
LENGTH_REQUIRED = 411,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7232#section-4.2
*
* The client has indicated preconditions in its headers which the server does not meet.
*/
PRECONDITION_FAILED = 412,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.11
*
* Request entity is larger than limits defined by server; the server might close the connection or return an Retry-After header field.
*/
REQUEST_TOO_LONG = 413,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.12
*
* The URI requested by the client is longer than the server is willing to interpret.
*/
REQUEST_URI_TOO_LONG = 414,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.13
*
* The media format of the requested data is not supported by the server, so the server is rejecting the request.
*/
UNSUPPORTED_MEDIA_TYPE = 415,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7233#section-4.4
*
* The range specified by the Range header field in the request can't be fulfilled; it's possible that the range is outside the size of the target URI's data.
*/
REQUESTED_RANGE_NOT_SATISFIABLE = 416,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.5.14
*
* This response code means the expectation indicated by the Expect request header field can't be met by the server.
*/
EXPECTATION_FAILED = 417,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc2324#section-2.3.2
*
* Any attempt to brew coffee with a teapot should result in the error code "418 I'm a teapot". The resulting entity body MAY be short and stout.
*/
IM_A_TEAPOT = 418,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.6
*
* The 507 (Insufficient Storage) status code means the method could not be performed on the resource because the server is unable to store the representation needed to successfully complete the request. This condition is considered to be temporary. If the request which received this status code was the result of a user action, the request MUST NOT be repeated until it is requested by a separate user action.
*/
INSUFFICIENT_SPACE_ON_RESOURCE = 419,
/**
* @deprecated
* Official Documentation @ https://tools.ietf.org/rfcdiff?difftype=--hwdiff&url2=draft-ietf-webdav-protocol-06.txt
*
* A deprecated response used by the Spring Framework when a method has failed.
*/
METHOD_FAILURE = 420,
/**
* Official Documentation @ https://datatracker.ietf.org/doc/html/rfc7540#section-9.1.2
*
* Defined in the specification of HTTP/2 to indicate that a server is not able to produce a response for the combination of scheme and authority that are included in the request URI.
*/
MISDIRECTED_REQUEST = 421,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.3
*
* The request was well-formed but was unable to be followed due to semantic errors.
*/
UNPROCESSABLE_ENTITY = 422,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.4
*
* The resource that is being accessed is locked.
*/
LOCKED = 423,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.5
*
* The request failed due to failure of a previous request.
*/
FAILED_DEPENDENCY = 424,
/**
* Official Documentation @ https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.15
*
* The server refuses to perform the request using the current protocol but might be willing to do so after the client upgrades to a different protocol.
*/
UPGRADE_REQUIRED = 426,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc6585#section-3
*
* The origin server requires the request to be conditional. Intended to prevent the 'lost update' problem, where a client GETs a resource's state, modifies it, and PUTs it back to the server, when meanwhile a third party has modified the state on the server, leading to a conflict.
*/
PRECONDITION_REQUIRED = 428,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc6585#section-4
*
* The user has sent too many requests in a given amount of time ("rate limiting").
*/
TOO_MANY_REQUESTS = 429,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc6585#section-5
*
* The server is unwilling to process the request because its header fields are too large. The request MAY be resubmitted after reducing the size of the request header fields.
*/
REQUEST_HEADER_FIELDS_TOO_LARGE = 431,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7725
*
* The user-agent requested a resource that cannot legally be provided, such as a web page censored by a government.
*/
UNAVAILABLE_FOR_LEGAL_REASONS = 451,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.6.1
*
* The server encountered an unexpected condition that prevented it from fulfilling the request.
*/
INTERNAL_SERVER_ERROR = 500,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.6.2
*
* The request method is not supported by the server and cannot be handled. The only methods that servers are required to support (and therefore that must not return this code) are GET and HEAD.
*/
NOT_IMPLEMENTED = 501,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.6.3
*
* This error response means that the server, while working as a gateway to get a response needed to handle the request, got an invalid response.
*/
BAD_GATEWAY = 502,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.6.4
*
* The server is not ready to handle the request. Common causes are a server that is down for maintenance or that is overloaded. Note that together with this response, a user-friendly page explaining the problem should be sent. This responses should be used for temporary conditions and the Retry-After: HTTP header should, if possible, contain the estimated time before the recovery of the service. The webmaster must also take care about the caching-related headers that are sent along with this response, as these temporary condition responses should usually not be cached.
*/
SERVICE_UNAVAILABLE = 503,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.6.5
*
* This error response is given when the server is acting as a gateway and cannot get a response in time.
*/
GATEWAY_TIMEOUT = 504,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc7231#section-6.6.6
*
* The HTTP version used in the request is not supported by the server.
*/
HTTP_VERSION_NOT_SUPPORTED = 505,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc2518#section-10.6
*
* The server has an internal configuration error: the chosen variant resource is configured to engage in transparent content negotiation itself, and is therefore not a proper end point in the negotiation process.
*/
INSUFFICIENT_STORAGE = 507,
/**
* Official Documentation @ https://tools.ietf.org/html/rfc6585#section-6
*
* The 511 status code indicates that the client needs to authenticate to gain network access.
*/
NETWORK_AUTHENTICATION_REQUIRED = 511
}

View file

@ -0,0 +1,74 @@
import { CollectionController } from '$lib/server/api/collections/collection.controller';
import { LoginController } from '$lib/server/api/login/login.controller';
import { MfaController } from '$lib/server/api/mfa/mfa.controller';
import { OAuthController } from '$lib/server/api/oauth/oauth.controller';
import { SignupController } from '$lib/server/api/signup/signup.controller';
import { WishlistController } from '$lib/server/api/wishlists/wishlist.controller';
import { inject, injectable } from '@needle-di/core';
import { contextStorage } from 'hono/context-storage';
import { requestId } from 'hono/request-id';
import { notFound, onError, serveEmojiFavicon } from 'stoker/middlewares';
import { RootController } from './common/factories/controllers.factory';
import { browserSessions } from './common/middleware/browser-session.middleware';
import { pinoLogger } from './common/middleware/pino-logger.middleware';
import { rateLimit } from './common/middleware/rate-limit.middleware';
import { sessionManagement } from './common/middleware/session-management.middleware';
import { generateId } from './common/utils/crypto';
import configureOpenAPI from './configure-open-api';
import { IamController } from './iam/iam.controller';
import { UsersController } from './users/users.controller';
@injectable()
export class ApplicationController extends RootController {
constructor(
private loginController = inject(LoginController),
private oAuthController = inject(OAuthController),
private signupController = inject(SignupController),
private mfaController = inject(MfaController),
private iamController = inject(IamController),
private usersController = inject(UsersController),
private wishlistController = inject(WishlistController),
private collectionController = inject(CollectionController),
) {
super();
}
routes() {
return this.controller
.get('/', (c) => {
return c.json({ status: 'ok' });
})
.get('/healthz', (c) => {
return c.json({ message: 'Server is healthy' });
})
.get('/rate-limit', rateLimit({ limit: 3, minutes: 1 }), (c) => {
return c.json({ message: 'Test!' });
});
}
registerControllers() {
const app = this.controller;
app.onError(onError);
app.notFound(notFound);
app
.basePath('/api')
.use(requestId({ generator: () => generateId() }))
.use(contextStorage())
.use(browserSessions)
.use(sessionManagement)
.use(serveEmojiFavicon('📝'))
.use(pinoLogger())
.route('/', this.routes())
.route('/iam', this.iamController.routes())
.route('/users', this.usersController.routes())
.route('/login', this.loginController.routes())
.route('/oauth', this.oAuthController.routes())
.route('/signup', this.signupController.routes())
.route('/wishlists', this.wishlistController.routes())
.route('/collections', this.collectionController.routes())
.route('/mfa', this.mfaController.routes());
configureOpenAPI(app);
return app;
}
}

View file

@ -0,0 +1,42 @@
import { inject, injectable } from '@needle-di/core';
import { ApplicationController } from './application.controller';
import { ConfigService } from './common/configs/config.service';
// import { StorageService } from './storage/storage.service';
@injectable()
export class ApplicationModule {
constructor(
private applicationController = inject(ApplicationController),
private configService = inject(ConfigService),
// private storageService = inject(StorageService),
) {}
async app() {
return this.applicationController.registerControllers();
}
async start() {
const app = this.app();
await this.onApplicationStartup();
// register shutdown hooks
process.on('SIGINT', this.onApplicationShutdown);
process.on('SIGTERM', this.onApplicationShutdown);
console.log(`Api started on port ${this.configService.envs.PORT}`);
return app;
}
private async onApplicationStartup() {
console.log('Application startup...');
// validate configs
this.configService.validateEnvs();
// configure storage service
// await this.storageService.configure();
}
private onApplicationShutdown() {
console.log('Shutting down...');
process.exit();
}
}

View file

@ -1,10 +1,10 @@
import { StatusCodes } from '$lib/constants/status-codes'; import { allCollections, getCollectionByCUID, numberOfCollections } from '$lib/server/api/collections/collection.routes';
import { CollectionsService } from '$lib/server/api/collections/collections.service';
import { requireFullAuth } from '$lib/server/api/common/middleware/require-auth.middleware';
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 { StatusCodes } from '$lib/utils/status-codes';
import { CollectionsService } from '$lib/server/api/services/collections.service'; import { inject, injectable } from '@needle-di/core';
import { openApi } from 'hono-zod-openapi'; import { openApi } from 'hono-zod-openapi';
import { injectable, inject } from '@needle-di/core';
import { requireFullAuth } from '../middleware/require-auth.middleware';
@injectable() @injectable()
export class CollectionController extends Controller { export class CollectionController extends Controller {

View file

@ -0,0 +1,68 @@
import cuidParamsSchema from '$lib/server/api/common/openapi/cuidParamsSchema';
import { unauthorizedSchema } from '$lib/server/api/common/utils/exceptions';
import { StatusCodes } from '$lib/utils/status-codes';
import { z } from '@hono/zod-openapi';
import { createErrorSchema } from 'stoker/openapi/schemas';
import { taggedAuthRoute } from '../common/openapi/create-auth-route';
import { selectCollectionSchema } from '../databases/postgres/tables';
const tag = 'Collection';
export const allCollections = taggedAuthRoute(tag, {
responses: {
[StatusCodes.OK]: {
description: 'User profile',
schema: selectCollectionSchema,
mediaType: 'application/json',
},
[StatusCodes.UNAUTHORIZED]: {
description: 'Unauthorized',
schema: createErrorSchema(unauthorizedSchema),
mediaType: 'application/json',
},
},
});
export const numberOfCollections = taggedAuthRoute(tag, {
responses: {
[StatusCodes.OK]: {
description: 'User profile',
mediaType: 'application/json',
},
[StatusCodes.UNAUTHORIZED]: {
description: 'Unauthorized',
schema: createErrorSchema(unauthorizedSchema),
mediaType: 'application/json',
},
},
});
export const getCollectionByCUID = taggedAuthRoute(tag, {
request: {
param: {
schema: cuidParamsSchema,
example: { cuid: 'z6uiuc9qz82xjf5dexc5kr2d' },
},
},
responses: {
[StatusCodes.OK]: {
description: 'User profile',
schema: selectCollectionSchema,
mediaType: 'application/json',
},
[StatusCodes.NOT_FOUND]: {
description: 'The collection does not exist',
schema: z.object({ message: z.string() }).openapi({
example: {
message: 'The collection does not exist',
},
}),
mediaType: 'application/json',
},
[StatusCodes.UNAUTHORIZED]: {
description: 'Unauthorized',
schema: createErrorSchema(unauthorizedSchema),
mediaType: 'application/json',
},
},
});

View file

@ -0,0 +1,83 @@
import { takeFirstOrThrow } from '$lib/server/api/common/utils/repository';
import { DrizzleService } from '$lib/server/api/databases/postgres/drizzle.service';
import { inject, injectable } from '@needle-di/core';
import { type InferInsertModel, eq } from 'drizzle-orm';
import { collections } from '../databases/postgres/tables';
export type CreateCollection = InferInsertModel<typeof collections>;
export type UpdateCollection = Partial<CreateCollection>;
@injectable()
export class CollectionsRepository {
constructor(private drizzle = inject(DrizzleService)) {}
async findAll(db = this.drizzle.db) {
return db.query.collections.findMany();
}
async findOneById(id: string, db = this.drizzle.db) {
return db.query.collections.findFirst({
where: eq(collections.id, id),
columns: {
cuid: true,
name: true,
},
});
}
async findOneByCuid(cuid: string, db = this.drizzle.db) {
return db.query.collections.findFirst({
where: eq(collections.cuid, cuid),
columns: {
cuid: true,
name: true,
},
});
}
async findOneByUserId(userId: string, db = this.drizzle.db) {
return db.query.collections.findFirst({
where: eq(collections.user_id, userId),
columns: {
cuid: true,
name: true,
},
});
}
async findAllByUserId(userId: string, db = this.drizzle.db) {
return db.query.collections.findMany({
where: eq(collections.user_id, userId),
columns: {
cuid: true,
name: true,
createdAt: true,
},
});
}
async findAllByUserIdWithDetails(userId: string, db = this.drizzle.db) {
return db.query.collections.findMany({
where: eq(collections.user_id, userId),
columns: {
cuid: true,
name: true,
},
with: {
collection_items: {
columns: {
cuid: true,
},
},
},
});
}
async create(data: CreateCollection, db = this.drizzle.db) {
return db.insert(collections).values(data).returning().then(takeFirstOrThrow);
}
async update(id: string, data: UpdateCollection, db = this.drizzle.db) {
return db.update(collections).set(data).where(eq(collections.id, id)).returning().then(takeFirstOrThrow);
}
}

View file

@ -0,0 +1,50 @@
import type { db } from '$lib/server/api/packages/drizzle';
import { generateRandomAnimalName } from '$lib/utils/randomDataUtil';
import { inject, injectable } from '@needle-di/core';
import { CollectionsRepository } from './collections.repository';
@injectable()
export class CollectionsService {
constructor(private collectionsRepository = inject(CollectionsRepository)) {}
async findOneByUserId(userId: string) {
return this.collectionsRepository.findOneByUserId(userId);
}
async findAllByUserId(userId: string) {
return this.collectionsRepository.findAllByUserId(userId);
}
async findAllByUserIdWithDetails(userId: string) {
return this.collectionsRepository.findAllByUserIdWithDetails(userId);
}
async findOneById(id: string) {
return this.collectionsRepository.findOneById(id);
}
async findOneByCuid(cuid: string) {
return this.collectionsRepository.findOneByCuid(cuid);
}
async createEmptyNoName(userId: string, trx: Parameters<Parameters<typeof db.transaction>[0]>[0] | null = null) {
return this.createEmpty(userId, null, trx);
}
async createEmpty(userId: string, name: string | null, trx: Parameters<Parameters<typeof db.transaction>[0]>[0] | null = null) {
if (!trx) {
return this.collectionsRepository.create({
user_id: userId,
name: name ?? generateRandomAnimalName(),
});
}
return this.collectionsRepository.create(
{
user_id: userId,
name: name ?? generateRandomAnimalName(),
},
trx,
);
}
}

View file

@ -0,0 +1,32 @@
import * as envs from '$env/static/private';
import { injectable } from '@needle-di/core';
import { z } from 'zod';
import { type EnvsDto, envsDto } from './dtos/env.dto';
@injectable()
export class ConfigService {
envs: EnvsDto;
constructor() {
// biome-ignore lint/style/noNonNullAssertion: <explanation>
this.envs = this.parseEnvs()!;
}
private parseEnvs() {
return envsDto.parse(envs);
}
validateEnvs() {
try {
return envsDto.parse(envs);
} catch (err) {
if (err instanceof z.ZodError) {
const { fieldErrors } = err.flatten();
const errorMessage = Object.entries(fieldErrors)
.map(([field, errors]) => (errors ? `${field}: ${errors.join(', ')}` : field))
.join('\n ');
throw new Error(`Missing environment variables:\n ${errorMessage}`);
}
}
}
}

View file

@ -0,0 +1,39 @@
import { z } from 'zod';
const stringBoolean = z.coerce
.string()
.transform((val) => {
return val === 'true';
})
.default('false');
export const envsDto = z.object({
DATABASE_USER: z.string(),
DATABASE_PASSWORD: z.string(),
DATABASE_HOST: z.string(),
DATABASE_PORT: z.coerce.number(),
DATABASE_DB: z.string(),
DB_MIGRATING: stringBoolean,
DB_SEEDING: stringBoolean,
DOMAIN: z.string(),
ENCRYPTION_KEY: z.string(),
ENV: z.enum(['dev', 'prod']),
GITHUB_CLIENT_ID: z.string(),
GITHUB_CLIENT_SECRET: z.string(),
GOOGLE_CLIENT_ID: z.string(),
GOOGLE_CLIENT_SECRET: z.string(),
LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
NODE_ENV: z.string().default('development'),
ORIGIN: z.string(),
PORT: z.number({ coerce: true }).default(5173),
PUBLIC_SITE_NAME: z.string(),
PUBLIC_SITE_URL: z.string(),
PUBLIC_UMAMI_DO_NOT_TRACK: z.string().default('true'),
PUBLIC_UMAMI_ID: z.string(),
PUBLIC_UMAMI_URL: z.string(),
REDIS_URL: z.string(),
SIGNING_SECRET: z.string(),
TWO_FACTOR_TIMEOUT: z.coerce.number().default(300000),
});
export type EnvsDto = z.infer<typeof envsDto>;

View file

@ -1,36 +1,36 @@
import env from "$lib/server/api/common/env"; import env from '$lib/server/api/common/env';
import type { AppBindings } from "$lib/server/api/common/types/hono"; import { validateAuthSession, verifyOrigin } from '$lib/server/api/common/middleware/auth.middleware';
import { validateAuthSession, verifyOrigin } from "$lib/server/api/middleware/auth.middleware"; import { pinoLogger } from '$lib/server/api/common/middleware/pino-logger.middleware';
import { pinoLogger } from "$lib/server/api/middleware/pino-logger.middleware"; import type { AppBindings } from '$lib/server/api/common/types/hono';
import { Hono } from "hono"; import { Hono } from 'hono';
import { cors } from "hono/cors"; import { cors } from 'hono/cors';
import { requestId } from "hono/request-id"; import { requestId } from 'hono/request-id';
import { notFound, onError, serveEmojiFavicon } from "stoker/middlewares"; import { notFound, onError, serveEmojiFavicon } from 'stoker/middlewares';
import { generateId } from "./utils/crypto"; import { generateId } from './utils/crypto';
export function createRouter() { export function createRouter() {
return new Hono<AppBindings>({ return new Hono<AppBindings>({
strict: false, strict: false,
}).basePath("/api"); }).basePath('/api');
} }
export default function createApp() { export default function createApp() {
const app = createRouter(); const app = createRouter();
app.use(verifyOrigin).use(validateAuthSession); app.use(verifyOrigin).use(validateAuthSession);
app.use(serveEmojiFavicon("📝")); app.use(serveEmojiFavicon('📝'));
app.use(requestId({ generator: () => generateId() })).use(pinoLogger()); app.use(requestId({ generator: () => generateId() })).use(pinoLogger());
app.notFound(notFound); app.notFound(notFound);
app.onError(onError); app.onError(onError);
app.use( app.use(
"/*", '/*',
cors({ cors({
origin: [env.ORIGIN], origin: [env.ORIGIN],
allowMethods: ["POST"], allowMethods: ['POST'],
allowHeaders: ["Content-Type"], allowHeaders: ['Content-Type'],
// credentials: true, // If you need to send cookies or HTTP authentication // credentials: true, // If you need to send cookies or HTTP authentication
}), }),
); );

View file

@ -1,6 +1,6 @@
import { config } from 'dotenv'; import { config } from 'dotenv';
import { expand } from 'dotenv-expand'; import { expand } from 'dotenv-expand';
import { z, type ZodError } from 'zod'; import { type ZodError, z } from 'zod';
expand(config()); expand(config());
@ -37,9 +37,9 @@ const EnvSchema = z.object({
TWO_FACTOR_TIMEOUT: z.coerce.number().default(300000), TWO_FACTOR_TIMEOUT: z.coerce.number().default(300000),
}); });
export type env = z.infer<typeof EnvSchema>; export type EnvsDto = z.infer<typeof EnvSchema>;
let env: env; let env: EnvsDto;
try { try {
env = EnvSchema.parse(process.env); env = EnvSchema.parse(process.env);

View file

@ -0,0 +1,13 @@
import { createHono } from '../utils/hono';
export abstract class Controller {
protected readonly controller: ReturnType<typeof createHono>;
constructor() {
this.controller = createHono();
}
abstract routes(): ReturnType<typeof createHono>;
}
export abstract class RootController extends Controller {
abstract registerControllers(): ReturnType<typeof createHono>;
}

View file

@ -0,0 +1,9 @@
import { Container } from '@needle-di/core';
import { DrizzleService } from '../../databases/postgres/drizzle.service';
export abstract class DrizzleRepository {
protected readonly drizzle: DrizzleService;
constructor() {
this.drizzle = new Container().get(DrizzleService);
}
}

View file

@ -0,0 +1,7 @@
import { Container } from '@needle-di/core';
import { RedisService } from '../../databases/redis/redis.service';
export abstract class RedisRepository<T extends string> {
protected readonly redis = new Container().get(RedisService);
readonly prefix: T | string = '';
}

View file

@ -0,0 +1,47 @@
import { Unauthorized } from '$lib/server/api/common/utils/exceptions';
import type { SessionDto } from '$lib/server/api/iam/sessions/dtos/session.dto';
import { SessionsService } from '$lib/server/api/iam/sessions/sessions.service';
import { Container } from '@needle-di/core';
import type { MiddlewareHandler } from 'hono';
import { createMiddleware } from 'hono/factory';
/* ---------------------------------- Types --------------------------------- */
type AuthStates = 'session' | 'none';
type AuthedReturnType = typeof authed;
type UnauthedReturnType = typeof unauthed;
/* ------------------- Overloaded function implementation ------------------- */
// we have to overload the implementation to provide the correct return type
export function authState(state: 'session'): AuthedReturnType;
export function authState(state: 'none'): UnauthedReturnType;
export function authState(state: AuthStates): AuthedReturnType | UnauthedReturnType {
if (state === 'session') return authed;
return unauthed;
}
// resolve dependencies from the container
const sessionService = new Container().get(SessionsService);
/* ------------------------------ Require Auth ------------------------------ */
const authed: MiddlewareHandler<{
Variables: {
session: SessionDto;
};
}> = createMiddleware(async (c, next) => {
if (!c.var.session) {
throw Unauthorized('You must be logged in to access this resource');
}
return next();
});
/* ---------------------------- Require Unauthed ---------------------------- */
const unauthed: MiddlewareHandler<{
Variables: {
session: null;
};
}> = createMiddleware(async (c, next) => {
if (c.var.session) {
throw Unauthorized('You must be logged out to access this resource');
}
return next();
});

View file

@ -0,0 +1,27 @@
import { Container } from '@needle-di/core';
import type { MiddlewareHandler } from 'hono';
import { getSignedCookie, setSignedCookie } from 'hono/cookie';
import { createMiddleware } from 'hono/factory';
import { ConfigService } from '../configs/config.service';
import { generateId } from '../utils/crypto';
export const browserSessions: MiddlewareHandler = createMiddleware(async (c, next) => {
const BROWSER_SESSION_COOKIE_NAME = '_id';
const container = new Container();
const configService = container.get(ConfigService);
const browserSessionCookie = await getSignedCookie(c, configService.envs.SIGNING_SECRET, BROWSER_SESSION_COOKIE_NAME);
let browserSessionId = browserSessionCookie;
if (!browserSessionCookie) {
browserSessionId = generateId();
setSignedCookie(c, BROWSER_SESSION_COOKIE_NAME, browserSessionId, configService.envs.SIGNING_SECRET, {
httpOnly: true,
sameSite: 'lax',
secure: configService.envs.ENV === 'prod',
path: '/',
});
}
c.set('browserSessionId', browserSessionId);
return next();
});

View file

@ -0,0 +1,35 @@
import { Container } from '@needle-di/core';
import type { Context } from 'hono';
import { rateLimiter } from 'hono-rate-limiter';
import { RedisStore } from 'rate-limit-redis';
import { RedisService } from '../../databases/redis/redis.service';
import type { HonoEnv } from '../utils/hono';
const container = new Container();
const client = container.get(RedisService).redis;
export function rateLimit({
limit,
minutes,
key = '',
}: {
limit: number;
minutes: number;
key?: string;
}) {
return rateLimiter({
windowMs: minutes * 60 * 1000, // every x minutes
limit, // Limit each IP to 100 requests per `window` (here, per 15 minutes).
standardHeaders: 'draft-6', // draft-6: `RateLimit-*` headers; draft-7: combined `RateLimit` header
keyGenerator: (c: Context<HonoEnv>) => {
const clientKey = c.var.session?.userId || c.req.header('x-forwarded-for');
const pathKey = key || c.req.routePath;
return `${clientKey}_${pathKey}`;
}, // Method to generate custom identifiers for clients.
// Redis store configuration
store: new RedisStore({
// @ts-expect-error - Known issue: the `call` function is not present in @types/ioredis
sendCommand: (...args: string[]) => client.call(...args),
}) as never,
});
}

View file

@ -1,4 +1,4 @@
import { Unauthorized } from '$lib/server/api/common/exceptions'; import { Unauthorized } from '$lib/server/api/common/utils/exceptions';
import type { Sessions, Users } 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';

View file

@ -0,0 +1,30 @@
import { Container } from '@needle-di/core';
import type { MiddlewareHandler } from 'hono';
import { createMiddleware } from 'hono/factory';
import { SessionsService } from '../../iam/sessions/sessions.service';
export const sessionManagement: MiddlewareHandler = createMiddleware(async (c, next) => {
const container = new Container();
const sessionService = container.get(SessionsService);
const sessionId = await sessionService.getSessionCookie();
if (!sessionId) {
c.set('session', null);
return next();
}
// Validate the session
const session = await sessionService.validateSession(sessionId);
// If the session is not found, delete the cookie
if (!session) sessionService.deleteSessionCookie();
// If the session is fresh, refresh the cookie with the new expiration date
if (session && session.fresh) sessionService.setSessionCookie(session);
// Set the session in the context
c.set('session', session);
// Continue to the next middleware
return next();
});

View file

@ -1,8 +1,8 @@
import { decodeBase64 } from '@oslojs/encoding';
import { createCipheriv, createDecipheriv } from 'crypto'; import { createCipheriv, createDecipheriv } from 'crypto';
import { DynamicBuffer } from '@oslojs/binary';
import { injectable } from '@needle-di/core'; import { injectable } from '@needle-di/core';
import { config } from '../common/config'; import { DynamicBuffer } from '@oslojs/binary';
import { decodeBase64 } from '@oslojs/encoding';
import { config } from '../config';
@injectable() @injectable()
export class EncryptionService { export class EncryptionService {

View file

@ -0,0 +1,38 @@
import { Container } from '@needle-di/core';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { HashingService } from './hashing.service';
describe('HashingService', () => {
let service: HashingService;
const container = new Container();
beforeAll(() => {
service = container.get(HashingService);
});
afterAll(() => {
vi.resetAllMocks();
});
describe('Create Hash', () => {
it('should create a hash', async () => {
const hash = await service.hash('111');
expect(hash).not.toBeUndefined();
expect(hash).not.toBeNull();
});
});
describe('Verify Hash', () => {
it('should verify a hash', async () => {
const hash = await service.hash('111');
const verifiable = await service.verify(hash, '111');
expect(verifiable).toBeTruthy();
});
it('should not verify a hash', async () => {
const hash = await service.hash('111');
const verifiable = await service.verify(hash, '222');
expect(verifiable).toBeFalsy();
});
});
});

View file

@ -0,0 +1,13 @@
import { injectable } from '@needle-di/core';
import { hash, verify } from 'argon2';
@injectable()
export class HashingService {
hash(data: string): Promise<string> {
return hash(data);
}
compare(data: string, encrypted: string): Promise<boolean> {
return verify(encrypted, data);
}
}

View file

@ -0,0 +1,22 @@
import { injectable } from '@needle-di/core';
import { getContext } from 'hono/context-storage';
import type { HonoEnv } from '../utils/hono';
@injectable()
export class RequestContextService {
getContext() {
return getContext<HonoEnv>();
}
getRequestId(): string {
return this.getContext().get('requestId');
}
getAuthedUserId() {
return this.getContext().get('session')?.userId;
}
getSession() {
return this.getContext().get('session');
}
}

View file

@ -0,0 +1,25 @@
import { inject, injectable } from '@needle-di/core';
import { generateId } from '../../common/utils/crypto';
import { HashingService } from '../../common/services/hashing.service';
@injectable()
export class VerificationCodesService {
constructor(private hashingService = inject(HashingService)) {}
async generateCodeWithHash() {
const verificationCode = this.generateCode();
const hashedVerificationCode = await this.hashingService.hash(verificationCode);
return { verificationCode, hashedVerificationCode };
}
verify(args: { verificationCode: string; hashedVerificationCode: string }) {
return this.hashingService.compare(args.verificationCode, args.hashedVerificationCode);
}
private generateCode() {
// alphabet with removed look-alike characters (0, 1, O, I)
const alphabet = '23456789ACDEFGHJKLMNPQRSTUVWXYZ';
// generate 6 character long random string
return generateId(6, alphabet);
}
}

View file

@ -1,8 +1,8 @@
import type { Session } from '$lib/server/api/services/sessions.service'; import type { Users } from '$lib/server/api/databases/postgres/tables';
import type { Session } from '$lib/server/api/iam/sessions/sessions.service';
import type { Hono } from 'hono'; import type { Hono } from 'hono';
import type { PinoLogger } from 'hono-pino'; import type { PinoLogger } from 'hono-pino';
import type { Promisify, RateLimitInfo } from 'hono-rate-limiter'; import type { Promisify, RateLimitInfo } from 'hono-rate-limiter';
import type { User } from 'lucia';
// export type AppOpenAPI = OpenAPIHono<AppBindings>; // export type AppOpenAPI = OpenAPIHono<AppBindings>;
export type AppOpenAPI = Hono<AppBindings>; export type AppOpenAPI = Hono<AppBindings>;
@ -11,7 +11,7 @@ export type AppBindings = {
Variables: { Variables: {
logger: PinoLogger; logger: PinoLogger;
session: Session | null; session: Session | null;
user: User | null; user: Users | null;
rateLimit: RateLimitInfo; rateLimit: RateLimitInfo;
rateLimitStore: { rateLimitStore: {
getKey?: (key: string) => Promisify<RateLimitInfo | undefined>; getKey?: (key: string) => Promisify<RateLimitInfo | undefined>;
@ -24,7 +24,7 @@ export type HonoTypes = {
Variables: { Variables: {
logger: PinoLogger; logger: PinoLogger;
session: Session | null; session: Session | null;
user: User | null; user: Users | null;
rateLimit: RateLimitInfo; rateLimit: RateLimitInfo;
rateLimitStore: { rateLimitStore: {
getKey?: (key: string) => Promisify<RateLimitInfo | undefined>; getKey?: (key: string) => Promisify<RateLimitInfo | undefined>;

View file

@ -1,12 +1,11 @@
import {config} from '$lib/server/api/common/config'; import { config } from '$lib/server/api/common/config';
import env from '$lib/server/api/common/env'; import env from '$lib/server/api/common/env';
import type {Context} from 'hono'; import type { Context } from 'hono';
import {setCookie} from 'hono/cookie'; import { setCookie } from 'hono/cookie';
import type {CookieOptions} from 'hono/utils/cookie'; import type { CookieOptions } from 'hono/utils/cookie';
import {TimeSpan} from 'oslo';
export const cookieMaxAge = 60 * 60 * 24 * 30; export const cookieMaxAge = 60 * 60 * 24 * 30;
export const cookieExpiresMilliseconds = new TimeSpan(2, 'w').milliseconds(); export const cookieExpiresMilliseconds = cookieMaxAge * 1000; // new TimeSpan(2, 'w').milliseconds();
export const cookieExpiresAt = new Date(Date.now() + cookieExpiresMilliseconds); export const cookieExpiresAt = new Date(Date.now() + cookieExpiresMilliseconds);
export const halfCookieExpiresMilliseconds = cookieExpiresMilliseconds / 2; export const halfCookieExpiresMilliseconds = cookieExpiresMilliseconds / 2;
export const halfCookieExpiresAt = new Date(Date.now() + halfCookieExpiresMilliseconds); export const halfCookieExpiresAt = new Date(Date.now() + halfCookieExpiresMilliseconds);

View file

@ -1,4 +1,4 @@
import { customAlphabet } from "nanoid"; import { customAlphabet } from 'nanoid';
// generateId is a function that returns a new unique identifier. // generateId is a function that returns a new unique identifier.
// ~4 million years or 30 trillion IDs needed, in order to have a 1% probability of at least one collision. // ~4 million years or 30 trillion IDs needed, in order to have a 1% probability of at least one collision.
@ -8,7 +8,7 @@ import { customAlphabet } from "nanoid";
// All hail king roomba, the first of his name, the unclean, king of the dust bunnies and the first allergens, lord of the seven corners, and protector of the realm. // All hail king roomba, the first of his name, the unclean, king of the dust bunnies and the first allergens, lord of the seven corners, and protector of the realm.
// https://zelark.github.io/nano-id-cc/ // https://zelark.github.io/nano-id-cc/
export function generateId(length = 16, alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") { export function generateId(length = 16, alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz') {
const nanoId = customAlphabet(alphabet, length); const nanoId = customAlphabet(alphabet, length);
return nanoId(); return nanoId();
} }

View file

@ -0,0 +1,53 @@
import { customType, timestamp } from 'drizzle-orm/pg-core';
import { NotFound } from './exceptions';
/* -------------------------------------------------------------------------- */
/* Repository */
/* -------------------------------------------------------------------------- */
// get the first element of an array or return null
export const takeFirst = <T>(values: T[]): T | null => {
return values.shift() || null;
};
// get the first element of an array or throw a 404 error
export const takeFirstOrThrow = <T>(values: T[]): T => {
const value = values.shift();
if (!value) throw NotFound('The requested resource was not found.');
return value;
};
/* -------------------------------------------------------------------------- */
/* Tables */
/* -------------------------------------------------------------------------- */
// custom type for citext
export const citext = customType<{ data: string }>({
dataType() {
return 'citext';
},
});
// custom type for generating an id
export const id = customType<{ data: string }>({
dataType() {
return 'text';
},
});
// timestamps for created_at and updated_at
export const timestamps = {
createdAt: timestamp('created_at', {
mode: 'date',
withTimezone: true,
})
.notNull()
.defaultNow(),
updatedAt: timestamp('updated_at', {
mode: 'date',
withTimezone: true,
})
.notNull()
.defaultNow()
.$onUpdateFn(() => new Date()),
};

View file

@ -1,7 +1,7 @@
import {StatusCodes} from '$lib/constants/status-codes'; import { StatusCodes } from '$lib/utils/status-codes';
import {HTTPException} from 'hono/http-exception'; import { HTTPException } from 'hono/http-exception';
import * as HttpStatusPhrases from 'stoker/http-status-phrases'; import * as HttpStatusPhrases from 'stoker/http-status-phrases';
import {createMessageObjectSchema} from 'stoker/openapi/schemas'; import { createMessageObjectSchema } from 'stoker/openapi/schemas';
export function TooManyRequests(message = 'Too many requests') { export function TooManyRequests(message = 'Too many requests') {
return new HTTPException(StatusCodes.TOO_MANY_REQUESTS, { message }); return new HTTPException(StatusCodes.TOO_MANY_REQUESTS, { message });

View file

@ -0,0 +1,20 @@
import { Hono } from 'hono';
import type { SessionDto } from '../../iam/sessions/dtos/session.dto';
import type { PinoLogger } from 'hono-pino';
export type AppOpenAPI = Hono<HonoEnv>;
export type HonoEnv = {
Variables: {
logger: PinoLogger;
session: SessionDto | null;
browserSessionId: string;
requestId: string;
};
};
export function createHono() {
return new Hono<HonoEnv>({
strict: false,
});
}

View file

@ -1,35 +1,7 @@
import {apiReference} from '@scalar/hono-api-reference'; import { apiReference } from '@scalar/hono-api-reference';
import { createOpenApiDocument } from 'hono-zod-openapi';
import type {AppOpenAPI} from '$lib/server/api/common/types/hono';
// import { createOpenApiDocument } from 'hono-zod-openapi';
import {createOpenApiDocument} from 'hono-zod-openapi';
import packageJSON from '../../../../package.json'; import packageJSON from '../../../../package.json';
import type { AppOpenAPI } from './common/utils/hono';
// export default function configureOpenAPI(app: AppOpenAPI) {
// app.doc('/doc', {
// openapi: '3.0.0',
// info: {
// title: 'Bored Game API',
// description: 'Bored Game API',
// version: packageJSON.version,
// },
// });
//
// app.get(
// '/reference',
// apiReference({
// theme: 'kepler',
// layout: 'classic',
// defaultHttpClient: {
// targetKey: 'javascript',
// clientKey: 'fetch',
// },
// spec: {
// url: '/api/doc',
// },
// }),
// );
// }
export default function configureOpenAPI(app: AppOpenAPI) { export default function configureOpenAPI(app: AppOpenAPI) {
createOpenApiDocument(app, { createOpenApiDocument(app, {
@ -48,7 +20,7 @@ export default function configureOpenAPI(app: AppOpenAPI) {
type: 'apiKey', type: 'apiKey',
name: 'session', name: 'session',
in: 'cookie', in: 'cookie',
} },
}, },
}, },
}); });

View file

@ -1,68 +0,0 @@
import {StatusCodes} from '$lib/constants/status-codes';
import {unauthorizedSchema} from '$lib/server/api/common/exceptions';
import cuidParamsSchema from '$lib/server/api/common/openapi/cuidParamsSchema';
import {z} from '@hono/zod-openapi';
import {createErrorSchema} from 'stoker/openapi/schemas';
import {taggedAuthRoute} from '../common/openapi/create-auth-route';
import {selectCollectionSchema} from '../databases/postgres/tables';
const tag = 'Collection';
export const allCollections = taggedAuthRoute(tag, {
responses: {
[StatusCodes.OK]: {
description: 'User profile',
schema: selectCollectionSchema,
mediaType: 'application/json',
},
[StatusCodes.UNAUTHORIZED]: {
description: 'Unauthorized',
schema: createErrorSchema(unauthorizedSchema),
mediaType: 'application/json',
},
},
});
export const numberOfCollections = taggedAuthRoute(tag, {
responses: {
[StatusCodes.OK]: {
description: 'User profile',
mediaType: 'application/json',
},
[StatusCodes.UNAUTHORIZED]: {
description: 'Unauthorized',
schema: createErrorSchema(unauthorizedSchema),
mediaType: 'application/json',
},
},
});
export const getCollectionByCUID = taggedAuthRoute(tag, {
request: {
param: {
schema: cuidParamsSchema,
example: { cuid: 'z6uiuc9qz82xjf5dexc5kr2d' },
},
},
responses: {
[StatusCodes.OK]: {
description: 'User profile',
schema: selectCollectionSchema,
mediaType: 'application/json',
},
[StatusCodes.NOT_FOUND]: {
description: 'The collection does not exist',
schema: z.object({ message: z.string() }).openapi({
example: {
message: 'The collection does not exist',
},
}),
mediaType: 'application/json',
},
[StatusCodes.UNAUTHORIZED]: {
description: 'Unauthorized',
schema: createErrorSchema(unauthorizedSchema),
mediaType: 'application/json',
},
},
});

View file

@ -1,145 +0,0 @@
import {StatusCodes} from '$lib/constants/status-codes';
import {unauthorizedSchema} from '$lib/server/api/common/exceptions';
import {selectUserSchema} from '$lib/server/api/databases/postgres/tables/users.table';
import {updateProfileDto} from '$lib/server/api/dtos/update-profile.dto';
import {createErrorSchema} from 'stoker/openapi/schemas';
import {taggedAuthRoute} from '../common/openapi/create-auth-route';
import {changePasswordDto} from '../dtos/change-password.dto';
import {verifyPasswordDto} from '../dtos/verify-password.dto';
const tag = 'IAM';
export const iam = taggedAuthRoute(tag, {
responses: {
[StatusCodes.OK]: {
description: 'User profile',
schema: selectUserSchema,
mediaType: 'application/json',
},
[StatusCodes.UNAUTHORIZED]: {
description: 'Unauthorized',
schema: createErrorSchema(unauthorizedSchema),
mediaType: 'application/json',
},
},
});
export const updateProfile = taggedAuthRoute(tag, {
request: {
json: updateProfileDto,
},
responses: {
[StatusCodes.OK]: {
description: 'Updated User',
schema: selectUserSchema,
mediaType: 'application/json',
},
[StatusCodes.BAD_REQUEST]: {
description: 'The validation error(s)',
schema: createErrorSchema(updateProfileDto),
mediaType: 'application/json',
},
[StatusCodes.UNPROCESSABLE_ENTITY]: {
description: 'Username already in use',
mediaType: 'application/json',
},
[StatusCodes.UNAUTHORIZED]: {
description: 'Unauthorized',
schema: createErrorSchema(unauthorizedSchema),
mediaType: 'application/json',
},
},
});
export const verifyPassword = taggedAuthRoute(tag, {
request: {
json: verifyPasswordDto,
},
responses: {
[StatusCodes.OK]: {
description: 'Password verified',
mediaType: 'application/json',
},
[StatusCodes.BAD_REQUEST]: {
description: 'The validation error(s)',
schema: createErrorSchema(verifyPasswordDto),
mediaType: 'application/json',
},
[StatusCodes.UNPROCESSABLE_ENTITY]: {
description: 'Incorrect password',
mediaType: 'application/json',
},
[StatusCodes.UNAUTHORIZED]: {
description: 'Unauthorized',
schema: createErrorSchema(unauthorizedSchema),
mediaType: 'application/json',
},
},
});
export const updatePassword = taggedAuthRoute(tag, {
request: {
json: changePasswordDto,
},
responses: {
[StatusCodes.OK]: {
description: 'Password updated',
mediaType: 'application/json',
},
[StatusCodes.BAD_REQUEST]: {
description: 'The validation error(s)',
schema: createErrorSchema(changePasswordDto),
mediaType: 'application/json',
},
[StatusCodes.UNAUTHORIZED]: {
description: 'Unauthorized',
schema: createErrorSchema(unauthorizedSchema),
mediaType: 'application/json',
},
[StatusCodes.FORBIDDEN]: {
description: 'Incorrect password',
mediaType: 'application/json',
},
[StatusCodes.INTERNAL_SERVER_ERROR]: {
description: 'Error updating password',
mediaType: 'application/json',
},
},
});
export const updateEmail = taggedAuthRoute(tag, {
responses: {
[StatusCodes.OK]: {
description: 'Email updated',
mediaType: 'application/json',
},
[StatusCodes.BAD_REQUEST]: {
description: 'The validation error(s)',
schema: createErrorSchema(changePasswordDto),
mediaType: 'application/json',
},
[StatusCodes.UNAUTHORIZED]: {
description: 'Unauthorized',
schema: createErrorSchema(unauthorizedSchema),
mediaType: 'application/json',
},
[StatusCodes.FORBIDDEN]: {
description: 'Cannot change email address',
mediaType: 'application/json',
},
},
});
export const logout = taggedAuthRoute(tag, {
responses: {
[StatusCodes.OK]: {
description: 'Logged out',
mediaType: 'application/json',
},
[StatusCodes.UNAUTHORIZED]: {
description: 'Unauthorized',
schema: createErrorSchema(unauthorizedSchema),
mediaType: 'application/json',
},
},
});

View file

@ -1,21 +0,0 @@
import {defineOpenApiOperation} from "hono-zod-openapi";
import {StatusCodes} from '$lib/constants/status-codes';
import {signinUsernameDto} from "../dtos/signin-username.dto";
import {createErrorSchema} from "stoker/openapi/schemas";
export const signinUsername = defineOpenApiOperation({
tags: ['Login'],
summary: 'Sign in with username',
description: 'Sign in with username',
responses: {
[StatusCodes.OK]: {
description: 'Sign in with username',
schema: signinUsernameDto,
},
[StatusCodes.UNPROCESSABLE_ENTITY]: {
description: 'The validation error(s)',
schema: createErrorSchema(signinUsernameDto),
mediaType: 'application/json',
}
}
});

View file

@ -1,125 +0,0 @@
import {Controller} from '$lib/server/api/common/types/controller';
import type {OAuthUser} from '$lib/server/api/common/types/oauth';
import {cookieExpiresAt, createSessionTokenCookie, setSessionCookie} from '$lib/server/api/common/utils/cookies';
import {OAuthService} from '$lib/server/api/services/oauth.service';
import {SessionsService} from '$lib/server/api/services/sessions.service';
import {github, google} from '$lib/server/auth';
import {OAuth2RequestError} from 'arctic';
import {getCookie} from 'hono/cookie';
import { injectable, inject } from "@needle-di/core";
@injectable()
export class OAuthController extends Controller {
constructor(
private oauthService = inject(OAuthService),
private sessionsService = inject(SessionsService),
) {
super();
}
routes() {
return this.controller
.get('/github', async (c) => {
try {
const code = c.req.query('code')?.toString() ?? null;
const state = c.req.query('state')?.toString() ?? null;
const storedState = getCookie(c).github_oauth_state ?? null;
if (!code || !state || !storedState || state !== storedState) {
return c.body(null, 400);
}
const tokens = await github.validateAuthorizationCode(code);
const githubUserResponse = await fetch('https://api.github.com/user', {
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
},
});
const githubUser: GitHubUser = await githubUserResponse.json();
const oAuthUser: OAuthUser = {
sub: `${githubUser.id}`,
username: githubUser.login,
email: undefined,
};
const userId = await this.oauthService.handleOAuthUser(oAuthUser, 'github');
const sessionToken = this.sessionsService.generateSessionToken();
const session = await this.sessionsService.createSession(sessionToken, userId, '', '', false, false);
const sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt);
setSessionCookie(c, sessionCookie);
return c.json({ message: 'ok' });
} catch (error) {
console.error(error);
// the specific error message depends on the provider
if (error instanceof OAuth2RequestError) {
// invalid code
return c.body(null, 400);
}
return c.body(null, 500);
}
})
.get('/google', async (c) => {
try {
const code = c.req.query('code')?.toString() ?? null;
const state = c.req.query('state')?.toString() ?? null;
const storedState = getCookie(c).google_oauth_state ?? null;
const storedCodeVerifier = getCookie(c).google_oauth_code_verifier ?? null;
if (!code || !storedState || !storedCodeVerifier || state !== storedState) {
return c.body(null, 400);
}
const tokens = await google.validateAuthorizationCode(code, storedCodeVerifier);
const googleUserResponse = await fetch('https://openidconnect.googleapis.com/v1/userinfo', {
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
},
});
const googleUser: GoogleUser = await googleUserResponse.json();
const oAuthUser: OAuthUser = {
sub: googleUser.sub,
given_name: googleUser.given_name,
family_name: googleUser.family_name,
picture: googleUser.picture,
username: googleUser.email,
email: googleUser.email,
email_verified: googleUser.email_verified,
};
const userId = await this.oauthService.handleOAuthUser(oAuthUser, 'google');
const sessionToken = this.sessionsService.generateSessionToken();
const session = await this.sessionsService.createSession(sessionToken, userId, '', '', false, false);
const sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt);
setSessionCookie(c, sessionCookie);
return c.json({ message: 'ok' });
} catch (error) {
console.error(error);
// the specific error message depends on the provider
if (error instanceof OAuth2RequestError) {
// invalid code
return c.body(null, 400);
}
return c.body(null, 500);
}
});
}
}
interface GitHubUser {
id: number;
login: string;
}
interface GoogleUser {
sub: string;
name: string;
given_name: string;
family_name: string;
picture: string;
email: string;
email_verified: boolean;
}

View file

@ -1,44 +0,0 @@
import 'reflect-metadata';
import {Controller} from '$lib/server/api/common/types/controller';
import {signupUsernameEmailDto} from '$lib/server/api/dtos/signup-username-email.dto';
import {limiter} from '$lib/server/api/middleware/rate-limiter.middleware';
import {LoginRequestsService} from '$lib/server/api/services/loginrequest.service';
import {SessionsService} from '$lib/server/api/services/sessions.service';
import {UsersService} from '$lib/server/api/services/users.service';
import {zValidator} from '@hono/zod-validator';
import {inject, injectable} from '@needle-di/core';
import {cookieExpiresAt, createSessionTokenCookie, setSessionCookie} from "$lib/server/api/common/utils/cookies";
@injectable()
export class SignupController extends Controller {
constructor(
private usersService = inject(UsersService),
private loginRequestService = inject(LoginRequestsService),
private sessionsService = inject(SessionsService),
) {
super();
}
routes() {
return this.controller.post('/', zValidator('json', signupUsernameEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
const { firstName, lastName, email, username, password, confirm_password } = await c.req.valid('json');
const existingUser = await this.usersService.findOneByUsername(username);
if (existingUser) {
return c.body('User already exists', 400);
}
const user = await this.usersService.create({ firstName, lastName, email, username, password, confirm_password });
if (!user) {
return c.body('Failed to create user', 500);
}
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);
return c.json({ message: 'ok' });
});
}
}

View file

@ -1,30 +0,0 @@
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();
}
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 });
});
}
}

View file

@ -0,0 +1,31 @@
import { ConfigService } from '$lib/server/api/common/configs/config.service';
import { inject, injectable } from '@needle-di/core';
import { type NodePgDatabase, drizzle } from 'drizzle-orm/node-postgres';
import pg from 'pg';
import * as schema from './tables';
@injectable()
export class DrizzleService {
pool: pg.Pool;
db: NodePgDatabase<typeof schema>;
readonly schema: typeof schema = schema;
constructor(private configService = inject(ConfigService)) {
const pool = new pg.Pool({
user: this.configService.envs.DATABASE_USER,
password: this.configService.envs.DATABASE_PASSWORD,
host: this.configService.envs.DATABASE_HOST,
port: Number(this.configService.envs.DATABASE_PORT).valueOf(),
database: this.configService.envs.DATABASE_DB,
ssl: false,
max: this.configService.envs.DB_MIGRATING || this.configService.envs.DB_SEEDING ? 1 : undefined,
});
this.pool = pool;
this.db = drizzle({
client: pool,
casing: 'snake_case',
schema,
logger: this.configService.envs.ENV !== 'prod',
});
}
}

View file

@ -1,8 +1,8 @@
import 'dotenv/config'; import 'dotenv/config';
import {migrate} from 'drizzle-orm/node-postgres/migrator'; import { migrate } from 'drizzle-orm/node-postgres/migrator';
import config from '../../../../../../drizzle.config'; import config from '../../../../../../drizzle.config';
import env from '../../common/env'; import env from '../../common/env';
import {DrizzleService} from '../../services/drizzle.service'; import { DrizzleService } from './drizzle.service';
const drizzleService = new DrizzleService(); const drizzleService = new DrizzleService();
@ -16,5 +16,5 @@ if (!env.DB_MIGRATING) {
await migrate(drizzleService.db, { migrationsFolder: config.out }); await migrate(drizzleService.db, { migrationsFolder: config.out });
console.log('Migrations complete'); console.log('Migrations complete');
await drizzleService.dispose(); await drizzleService.pool.end();
process.exit(); process.exit();

View file

@ -1,7 +1,7 @@
import {getTableName, sql, type Table} from 'drizzle-orm'; import { type Table, getTableName, sql } from 'drizzle-orm';
import type {NodePgDatabase} from 'drizzle-orm/node-postgres'; import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
import env from '../../common/env'; import env from '../../common/env';
import {DrizzleService} from '../../services/drizzle.service'; import { DrizzleService } from './drizzle.service';
import * as seeds from './seeds'; import * as seeds from './seeds';
import * as schema from './tables'; import * as schema from './tables';
@ -49,5 +49,5 @@ for (const table of [
await seeds.roles(drizzleService.db); await seeds.roles(drizzleService.db);
await seeds.users(drizzleService.db); await seeds.users(drizzleService.db);
await drizzleService.dispose(); await drizzleService.pool.end();
process.exit(); process.exit();

View file

@ -1,6 +1,6 @@
import {eq} from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import type {NodePgDatabase} from 'drizzle-orm/node-postgres'; import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
import {HashingService} from '../../../services/hashing.service'; import { HashingService } from '../../../common/services/hashing.service';
import * as schema from '../tables'; import * as schema from '../tables';
import users from './data/users.json'; import users from './data/users.json';

View file

@ -1,10 +1,13 @@
import {createId as cuid2} from '@paralleldrive/cuid2'; import { createId as cuid2 } from '@paralleldrive/cuid2';
import {type InferSelectModel, relations} from 'drizzle-orm'; import { type InferSelectModel, relations, getTableColumns } from 'drizzle-orm';
import {boolean, pgTable, text, uuid} from 'drizzle-orm/pg-core'; import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core';
import {createSelectSchema} from 'drizzle-zod'; import { createSelectSchema } from 'drizzle-zod';
import {timestamps} from '../../../common/utils/table'; import { timestamps } from '../../../common/utils/table';
import {user_roles} from './userRoles.table'; import { user_roles } from './userRoles.table';
/* -------------------------------------------------------------------------- */
/* Table */
/* -------------------------------------------------------------------------- */
export const usersTable = pgTable('users', { export const usersTable = pgTable('users', {
id: uuid().primaryKey().defaultRandom(), id: uuid().primaryKey().defaultRandom(),
cuid: text() cuid: text()
@ -23,10 +26,30 @@ export const usersTable = pgTable('users', {
...timestamps, ...timestamps,
}); });
/* -------------------------------------------------------------------------- */
/* Relations */
/* -------------------------------------------------------------------------- */
export const userRelations = relations(usersTable, ({ many }) => ({ export const userRelations = relations(usersTable, ({ many }) => ({
user_roles: many(user_roles), user_roles: many(user_roles),
})); }));
/* -------------------------------------------------------------------------- */
/* Types */
/* -------------------------------------------------------------------------- */
export const selectUserSchema = createSelectSchema(usersTable); export const selectUserSchema = createSelectSchema(usersTable);
export type Users = InferSelectModel<typeof usersTable>; export type Users = InferSelectModel<typeof usersTable>;
export type UserWithRelations = Users & {};
const userColumns = getTableColumns(usersTable);
export const publicUserColumns = {
id: userColumns.id,
cuid: userColumns.cuid,
username: userColumns.username,
first_name: userColumns.first_name,
last_name: userColumns.last_name,
picture: userColumns.picture,
theme: userColumns.theme,
...timestamps,
};

View file

@ -0,0 +1,35 @@
import { config } from '$lib/server/api/common/config';
import { injectable } from '@needle-di/core';
import { Redis } from 'ioredis';
@injectable()
export class RedisService {
readonly redis: Redis;
constructor() {
this.redis = new Redis(config.redis.url, {
maxRetriesPerRequest: null,
});
}
async get(data: { prefix: string; key: string }): Promise<string | null> {
return this.redis.get(`${data.prefix}:${data.key}`);
}
async set(data: { prefix: string; key: string; value: string }): Promise<void> {
await this.redis.set(`${data.prefix}:${data.key}`, data.value);
}
async delete(data: { prefix: string; key: string }): Promise<void> {
await this.redis.del(`${data.prefix}:${data.key}`);
}
async setWithExpiry(data: {
prefix: string;
key: string;
value: string;
expiry: number;
}): Promise<void> {
await this.redis.set(`${data.prefix}:${data.key}`, data.value, 'EXAT', Math.floor(data.expiry));
}
}

View file

@ -1,20 +1,20 @@
import { StatusCodes } from '$lib/constants/status-codes'; import { limiter } from '$lib/server/api/common/middleware/rate-limit.middleware';
import { requireFullAuth, requireTempAuth } from '$lib/server/api/common/middleware/require-auth.middleware';
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 { IamService } from '$lib/server/api/iam/iam.service';
import { IamService } from '$lib/server/api/services/iam.service'; import { SessionsService } from '$lib/server/api/iam/sessions/sessions.service';
import { LoginRequestsService } from '$lib/server/api/services/loginrequest.service'; import { LoginRequestsService } from '$lib/server/api/login/loginrequest.service';
import { SessionsService } from '$lib/server/api/services/sessions.service'; import { StatusCodes } from '$lib/utils/status-codes';
import { zValidator } from '@hono/zod-validator'; import { zValidator } from '@hono/zod-validator';
import { inject, injectable } from '@needle-di/core';
import { openApi } from 'hono-zod-openapi'; import { openApi } from 'hono-zod-openapi';
import { injectable, inject } from '@needle-di/core'; import { UsersRepository } from '../users/users.repository';
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 {

View file

@ -0,0 +1,145 @@
import { unauthorizedSchema } from '$lib/server/api/common/utils/exceptions';
import { selectUserSchema } from '$lib/server/api/databases/postgres/tables/users.table';
import { updateProfileDto } from '$lib/server/api/dtos/update-profile.dto';
import { StatusCodes } from '$lib/utils/status-codes';
import { createErrorSchema } from 'stoker/openapi/schemas';
import { taggedAuthRoute } from '../common/openapi/create-auth-route';
import { changePasswordDto } from '../dtos/change-password.dto';
import { verifyPasswordDto } from '../dtos/verify-password.dto';
const tag = 'IAM';
export const iam = taggedAuthRoute(tag, {
responses: {
[StatusCodes.OK]: {
description: 'User profile',
schema: selectUserSchema,
mediaType: 'application/json',
},
[StatusCodes.UNAUTHORIZED]: {
description: 'Unauthorized',
schema: createErrorSchema(unauthorizedSchema),
mediaType: 'application/json',
},
},
});
export const updateProfile = taggedAuthRoute(tag, {
request: {
json: updateProfileDto,
},
responses: {
[StatusCodes.OK]: {
description: 'Updated User',
schema: selectUserSchema,
mediaType: 'application/json',
},
[StatusCodes.BAD_REQUEST]: {
description: 'The validation error(s)',
schema: createErrorSchema(updateProfileDto),
mediaType: 'application/json',
},
[StatusCodes.UNPROCESSABLE_ENTITY]: {
description: 'Username already in use',
mediaType: 'application/json',
},
[StatusCodes.UNAUTHORIZED]: {
description: 'Unauthorized',
schema: createErrorSchema(unauthorizedSchema),
mediaType: 'application/json',
},
},
});
export const verifyPassword = taggedAuthRoute(tag, {
request: {
json: verifyPasswordDto,
},
responses: {
[StatusCodes.OK]: {
description: 'Password verified',
mediaType: 'application/json',
},
[StatusCodes.BAD_REQUEST]: {
description: 'The validation error(s)',
schema: createErrorSchema(verifyPasswordDto),
mediaType: 'application/json',
},
[StatusCodes.UNPROCESSABLE_ENTITY]: {
description: 'Incorrect password',
mediaType: 'application/json',
},
[StatusCodes.UNAUTHORIZED]: {
description: 'Unauthorized',
schema: createErrorSchema(unauthorizedSchema),
mediaType: 'application/json',
},
},
});
export const updatePassword = taggedAuthRoute(tag, {
request: {
json: changePasswordDto,
},
responses: {
[StatusCodes.OK]: {
description: 'Password updated',
mediaType: 'application/json',
},
[StatusCodes.BAD_REQUEST]: {
description: 'The validation error(s)',
schema: createErrorSchema(changePasswordDto),
mediaType: 'application/json',
},
[StatusCodes.UNAUTHORIZED]: {
description: 'Unauthorized',
schema: createErrorSchema(unauthorizedSchema),
mediaType: 'application/json',
},
[StatusCodes.FORBIDDEN]: {
description: 'Incorrect password',
mediaType: 'application/json',
},
[StatusCodes.INTERNAL_SERVER_ERROR]: {
description: 'Error updating password',
mediaType: 'application/json',
},
},
});
export const updateEmail = taggedAuthRoute(tag, {
responses: {
[StatusCodes.OK]: {
description: 'Email updated',
mediaType: 'application/json',
},
[StatusCodes.BAD_REQUEST]: {
description: 'The validation error(s)',
schema: createErrorSchema(changePasswordDto),
mediaType: 'application/json',
},
[StatusCodes.UNAUTHORIZED]: {
description: 'Unauthorized',
schema: createErrorSchema(unauthorizedSchema),
mediaType: 'application/json',
},
[StatusCodes.FORBIDDEN]: {
description: 'Cannot change email address',
mediaType: 'application/json',
},
},
});
export const logout = taggedAuthRoute(tag, {
responses: {
[StatusCodes.OK]: {
description: 'Logged out',
mediaType: 'application/json',
},
[StatusCodes.UNAUTHORIZED]: {
description: 'Unauthorized',
schema: createErrorSchema(unauthorizedSchema),
mediaType: 'application/json',
},
},
});

View file

@ -2,8 +2,8 @@ import type { ChangePasswordDto } from '$lib/server/api/dtos/change-password.dto
import type { UpdateEmailDto } from '$lib/server/api/dtos/update-email.dto'; import type { UpdateEmailDto } from '$lib/server/api/dtos/update-email.dto';
import type { UpdateProfileDto } from '$lib/server/api/dtos/update-profile.dto'; import type { UpdateProfileDto } from '$lib/server/api/dtos/update-profile.dto';
import type { VerifyPasswordDto } from '$lib/server/api/dtos/verify-password.dto'; import type { VerifyPasswordDto } from '$lib/server/api/dtos/verify-password.dto';
import { SessionsService } from '$lib/server/api/services/sessions.service'; import { SessionsService } from '$lib/server/api/iam/sessions/sessions.service';
import { UsersService } from '$lib/server/api/services/users.service'; import { UsersService } from '$lib/server/api/users/users.service';
import { inject, injectable } from '@needle-di/core'; import { inject, injectable } from '@needle-di/core';
@injectable() @injectable()

View file

@ -0,0 +1,14 @@
import { z } from 'zod';
export const createSessionDto = z.object({
id: z.string(),
userId: z.string(),
createdAt: z.date({ coerce: true }),
expiresAt: z.date({ coerce: true }),
ipCountry: z.string(),
ipAddress: z.string(),
twoFactorEnabled: z.boolean(),
twoFactorVerified: z.boolean(),
});
export type CreateSessionDto = z.infer<typeof createSessionDto>;

View file

@ -0,0 +1,13 @@
import { z } from 'zod';
export const sessionDto = z.object({
id: z.string(),
userId: z.string(),
expiresAt: z.date(),
createdAt: z.date(),
fresh: z.boolean(),
twoFactorEnabled: z.boolean(),
twoFactorVerified: z.boolean(),
});
export type SessionDto = z.infer<typeof sessionDto>;

View file

@ -0,0 +1,31 @@
import { RedisRepository } from '$lib/server/api/common/factories/redis-repository.factory';
import { type CreateSessionDto, createSessionDto } from '$lib/server/api/iam/sessions/dtos/create-session-dto';
import { injectable } from '@needle-di/core';
@injectable()
export class SessionsRepository extends RedisRepository<'session'> {
async get(id: string): Promise<CreateSessionDto | null> {
const response = await this.redis.get({ prefix: this.prefix, key: id });
if (!response) return null;
return createSessionDto.parse(JSON.parse(response));
}
async findOneById(id: string): Promise<CreateSessionDto | null> {
const response = await this.redis.get({ prefix: this.prefix, key: id });
if (!response) return null;
return createSessionDto.parse(JSON.parse(response));
}
delete(id: string) {
return this.redis.delete({ prefix: this.prefix, key: id });
}
create(createSessionDto: CreateSessionDto) {
return this.redis.setWithExpiry({
prefix: this.prefix,
key: createSessionDto.id,
value: JSON.stringify(createSessionDto),
expiry: Math.floor(+createSessionDto.expiresAt / 1000),
});
}
}

View file

@ -0,0 +1,92 @@
import { ConfigService } from '$lib/server/api/common/configs/config.service';
import { RequestContextService } from '$lib/server/api/common/services/request-context.service';
import { generateId } from '$lib/server/api/common/utils/crypto';
import type { CreateSessionDto } from '$lib/server/api/iam/sessions/dtos/create-session-dto';
import type { SessionDto } from '$lib/server/api/iam/sessions/dtos/session.dto';
import { SessionsRepository } from '$lib/server/api/iam/sessions/sessions.repository';
import { UsersService } from '$lib/server/api/users/users.service';
import { inject, injectable } from '@needle-di/core';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { deleteCookie, getSignedCookie, setSignedCookie } from 'hono/cookie';
dayjs.extend(relativeTime);
@injectable()
export class SessionsService {
private readonly sessionCookieName = 'session';
constructor(
private sessionsRepository = inject(SessionsRepository),
private requestContextService = inject(RequestContextService),
private configService = inject(ConfigService),
private usersService = inject(UsersService),
) {}
setSessionCookie(session: SessionDto) {
return setSignedCookie(this.requestContextService.getContext(), this.sessionCookieName, session.id, this.configService.envs.SIGNING_SECRET, {
httpOnly: true,
sameSite: 'lax',
secure: this.configService.envs.ENV === 'prod',
path: '/',
expires: session.expiresAt,
});
}
async getSessionCookie(): Promise<string | null> {
const session = await getSignedCookie(this.requestContextService.getContext(), this.configService.envs.SIGNING_SECRET, this.sessionCookieName);
if (!session) return null;
return session;
}
deleteSessionCookie() {
return deleteCookie(this.requestContextService.getContext(), this.sessionCookieName);
}
async createSession(userId: string, twoFactorVerified = false, ipCountry = 'unknown', ipAddress = 'unknown'): Promise<SessionDto> {
const user = await this.usersService.findOneById(userId);
if (!user) throw new Error('User not found');
const session: CreateSessionDto = {
id: this.generateSessionToken(),
userId,
createdAt: dayjs().toDate(),
expiresAt: dayjs().add(30, 'day').toDate(),
ipCountry,
ipAddress,
twoFactorEnabled: user.mfa_enabled,
twoFactorVerified,
};
await this.sessionsRepository.create(session);
return { ...session, fresh: true };
}
async validateSession(sessionId: string): Promise<SessionDto | null> {
// Check if session exists
const existingSession = await this.sessionsRepository.get(sessionId);
// If session does not exist, return null
if (!existingSession) return null;
// If session exists, check if it should be extended
const shouldExtendSession = dayjs(existingSession.expiresAt).diff(Date.now(), 'day') < 15;
// If session should be extended, update the session in the database
if (shouldExtendSession) {
existingSession.expiresAt = dayjs().add(30, 'day').toDate();
await this.sessionsRepository.create({ ...existingSession });
return { ...existingSession, fresh: true };
}
return { ...existingSession, fresh: false };
}
async invalidateSession(sessionId: string): Promise<void> {
await this.sessionsRepository.delete(sessionId);
}
private generateSessionToken(): string {
return generateId();
}
}

View file

@ -1,51 +1,21 @@
import createApp from "$lib/server/api/common/create-app"; import { Container } from '@needle-di/core';
import configureOpenAPI from "$lib/server/api/configure-open-api"; import { extendZodWithOpenApi } from 'hono-zod-openapi';
import { CollectionController } from "$lib/server/api/controllers/collection.controller"; import { z } from 'zod';
import { MfaController } from "$lib/server/api/controllers/mfa.controller"; import { ApplicationController } from './application.controller';
import { OAuthController } from "$lib/server/api/controllers/oauth.controller"; import { ApplicationModule } from './application.module';
import { SignupController } from "$lib/server/api/controllers/signup.controller";
import { UserController } from "$lib/server/api/controllers/user.controller";
import { WishlistController } from "$lib/server/api/controllers/wishlist.controller";
import { AuthCleanupJobs } from "$lib/server/api/jobs/auth-cleanup.job";
import { Container } from "@needle-di/core";
import { extendZodWithOpenApi } from "hono-zod-openapi";
import { hc } from "hono/client";
import { z } from "zod";
import { config } from "./common/config";
import { IamController } from "./controllers/iam.controller";
import { LoginController } from "./controllers/login.controller";
extendZodWithOpenApi(z); extendZodWithOpenApi(z);
const container = new Container(); const applicationController = new Container().get(ApplicationController);
const applicationModule = new Container().get(ApplicationModule);
export const app = createApp(); /* ------------------------------ startServer ------------------------------ */
export function startServer() {
return applicationModule.start();
}
/* -------------------------------------------------------------------------- */ /* ----------------------------------- api ---------------------------------- */
/* Routes */ export const routes = applicationController.registerControllers();
/* -------------------------------------------------------------------------- */
const routes = app
.route("/me", container.get(IamController).routes())
.route("/user", container.get(UserController).routes())
.route("/login", container.get(LoginController).routes())
.route("/oauth", container.get(OAuthController).routes())
.route("/signup", container.get(SignupController).routes())
.route("/wishlists", container.get(WishlistController).routes())
.route("/collections", container.get(CollectionController).routes())
.route("/mfa", container.get(MfaController).routes())
.get("/", (c) => c.json({ message: "Server is healthy" }));
configureOpenAPI(app); /* ---------------------------------- Types --------------------------------- */
/* -------------------------------------------------------------------------- */
/* Cron Jobs */
/* -------------------------------------------------------------------------- */
container.get(AuthCleanupJobs).deleteStaleEmailVerificationRequests();
container.get(AuthCleanupJobs).deleteStaleLoginRequests();
/* -------------------------------------------------------------------------- */
/* Exports */
/* -------------------------------------------------------------------------- */
export const rpc = hc<typeof routes>(config.api.origin);
export type ApiClient = typeof rpc;
export type ApiRoutes = typeof routes; export type ApiRoutes = typeof routes;

View file

@ -1,13 +1,13 @@
import { limiter } from '$lib/server/api/common/middleware/rate-limit.middleware';
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/iam/sessions/sessions.service';
import { zValidator } from '@hono/zod-validator'; import { zValidator } from '@hono/zod-validator';
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 { openApi } from 'hono-zod-openapi';
import { LoginRequestsService } from '../services/loginrequest.service';
import { signinUsername } from './login.routes'; import { signinUsername } from './login.routes';
import { LoginRequestsService } from './loginrequest.service';
@injectable() @injectable()
export class LoginController extends Controller { export class LoginController extends Controller {

View file

@ -0,0 +1,21 @@
import { StatusCodes } from '$lib/utils/status-codes';
import { defineOpenApiOperation } from 'hono-zod-openapi';
import { createErrorSchema } from 'stoker/openapi/schemas';
import { signinUsernameDto } from '../dtos/signin-username.dto';
export const signinUsername = defineOpenApiOperation({
tags: ['Login'],
summary: 'Sign in with username',
description: 'Sign in with username',
responses: {
[StatusCodes.OK]: {
description: 'Sign in with username',
schema: signinUsernameDto,
},
[StatusCodes.UNPROCESSABLE_ENTITY]: {
description: 'The validation error(s)',
schema: createErrorSchema(signinUsernameDto),
mediaType: 'application/json',
},
},
});

View file

@ -1,14 +1,14 @@
import { DrizzleService } from '$lib/server/api/databases/postgres/drizzle.service';
import type { SigninUsernameDto } from '$lib/server/api/dtos/signin-username.dto'; 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/iam/sessions/sessions.service';
import type { HonoRequest } from 'hono';
import { inject, injectable } from '@needle-di/core'; import { inject, injectable } from '@needle-di/core';
import { BadRequest, NotFound } from '../common/exceptions'; import type { HonoRequest } from 'hono';
import { BadRequest, NotFound } from '../common/utils/exceptions';
import type { Credentials } from '../databases/postgres/tables'; import type { Credentials } from '../databases/postgres/tables';
import { CredentialsRepository } from '../repositories/credentials.repository'; import { MailerService } from '../services/mailer.service';
import { UsersRepository } from '../repositories/users.repository'; import { TokensService } from '../services/tokens.service';
import { MailerService } from './mailer.service'; import { CredentialsRepository } from '../users/credentials.repository';
import { TokensService } from './tokens.service'; import { UsersRepository } from '../users/users.repository';
import { DrizzleService } from '$lib/server/api/services/drizzle.service';
@injectable() @injectable()
export class LoginRequestsService { export class LoginRequestsService {

View file

@ -1,17 +1,17 @@
import { StatusCodes } from '$lib/constants/status-codes'; import { requireFullAuth, requireTempAuth } from '$lib/server/api/common/middleware/require-auth.middleware';
import { Controller } from '$lib/server/api/common/types/controller'; import { Controller } from '$lib/server/api/common/types/controller';
import { verifyTotpDto } from '$lib/server/api/dtos/verify-totp.dto'; import { verifyTotpDto } from '$lib/server/api/dtos/verify-totp.dto';
import { RecoveryCodesService } from '$lib/server/api/services/recovery-codes.service'; import { TotpService } from '$lib/server/api/mfa/totp.service';
import { TotpService } from '$lib/server/api/services/totp.service'; import { RecoveryCodesService } from '$lib/server/api/users/recovery-codes.service';
import { UsersService } from '$lib/server/api/services/users.service'; import { UsersService } from '$lib/server/api/users/users.service';
import { StatusCodes } from '$lib/utils/status-codes';
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 { requireFullAuth, requireTempAuth } from '../middleware/require-auth.middleware';
import { createTwoFactorSchema } from '../dtos/create-totp.dto';
import { decodeBase64 } from '@oslojs/encoding'; import { decodeBase64 } from '@oslojs/encoding';
import { LoginRequestsService } from '../services/loginrequest.service';
import { cookieExpiresAt, createSessionTokenCookie, setSessionCookie } from '../common/utils/cookies'; import { cookieExpiresAt, createSessionTokenCookie, setSessionCookie } from '../common/utils/cookies';
import { CredentialsType } from '../databases/postgres/tables';
import { createTwoFactorSchema } from '../dtos/create-totp.dto';
import { LoginRequestsService } from '../login/loginrequest.service';
@injectable() @injectable()
export class MfaController extends Controller { export class MfaController extends Controller {

View file

@ -1,15 +1,15 @@
import { CredentialsRepository } from '$lib/server/api/repositories/credentials.repository';
import { inject, injectable } from '@needle-di/core'; import { inject, injectable } from '@needle-di/core';
import { decodeBase64, encodeBase64 } from "@oslojs/encoding"; import { decodeBase64, encodeBase64 } from '@oslojs/encoding';
import { generateTOTP, verifyTOTP } from '@oslojs/otp'; import { generateTOTP, verifyTOTP } from '@oslojs/otp';
import { EncryptionService } from '../common/services/encryption.service';
import type { CredentialsType } from '../databases/postgres/tables'; import type { CredentialsType } from '../databases/postgres/tables';
import { EncryptionService } from './encryption.service'; import { CredentialsRepository } from '../users/credentials.repository';
@injectable() @injectable()
export class TotpService { export class TotpService {
constructor( constructor(
private credentialsRepository = inject(CredentialsRepository), private credentialsRepository = inject(CredentialsRepository),
private encryptionService = inject(EncryptionService) private encryptionService = inject(EncryptionService),
) {} ) {}
async findOneByUserId(userId: string) { async findOneByUserId(userId: string) {

View file

@ -1,53 +0,0 @@
import {
cookieExpiresAt,
cookieName,
createBlankSessionTokenCookie,
createSessionTokenCookie,
type SessionCookie,
setSessionCookie,
} from '$lib/server/api/common/utils/cookies';
import {SessionsService} from '$lib/server/api/services/sessions.service';
import type {MiddlewareHandler} from 'hono';
import {getCookie} from 'hono/cookie';
import {createMiddleware} from 'hono/factory';
import {verifyRequestOrigin} from 'oslo/request';
import { Container } from '@needle-di/core';
import type {AppBindings} from '../common/types/hono';
// resolve dependencies from the container
const container = new Container();
const sessionService = container.get(SessionsService);
// CSRF protection middleware
export const verifyOrigin: MiddlewareHandler<AppBindings> = createMiddleware(async (c, next) => {
if (c.req.method === 'GET') {
return next();
}
const originHeader = c.req.header('Origin') ?? null;
const hostHeader = c.req.header('Host') ?? null;
if (!originHeader || !hostHeader || !verifyRequestOrigin(originHeader, [hostHeader])) {
return c.body(null, 403);
}
return next();
});
export const validateAuthSession: MiddlewareHandler<AppBindings> = createMiddleware(async (c, next) => {
const sessionId = getCookie(c, cookieName) ?? null;
if (!sessionId) {
c.set('user', null);
c.set('session', null);
return next();
}
const { session, user } = await sessionService.validateSessionToken(sessionId);
let sessionCookie: SessionCookie;
if (session !== null) {
sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt);
} else {
sessionCookie = createBlankSessionTokenCookie();
}
setSessionCookie(c, sessionCookie);
c.set('session', session);
c.set('user', user);
return next();
});

View file

@ -1,36 +0,0 @@
import {rateLimiter} from 'hono-rate-limiter';
import {RedisStore} from 'rate-limit-redis';
import { Container } from '@needle-di/core';
import type {AppBindings} from '../common/types/hono';
import {RedisService} from '../services/redis.service';
const container = new Container();
// resolve dependencies from the container
const { client } = container.get(RedisService);
export function limiter({
limit,
minutes,
key = '',
}: {
limit: number;
minutes: number;
key?: string;
}) {
return rateLimiter({
windowMs: minutes * 60 * 1000, // every x minutes
limit, // Limit each IP to 100 requests per `window` (here, per 15 minutes).
standardHeaders: 'draft-6', // draft-6: `RateLimit-*` headers; draft-7: combined `RateLimit` header
keyGenerator: (c) => {
const vars = c.var as AppBindings['Variables'];
const clientKey = vars.user?.id || c.req.header('x-forwarded-for');
const pathKey = key || c.req.routePath;
return `${clientKey}_${pathKey}`;
}, // Method to generate custom identifiers for clients.
// Redis store configuration
store: new RedisStore({
// @ts-expect-error - Known issue: the `call` function is not present in @types/ioredis
sendCommand: (...args: string[]) => client.call(...args),
}) as any,
});
}

View file

@ -0,0 +1,125 @@
import { Controller } from '$lib/server/api/common/types/controller';
import type { OAuthUser } from '$lib/server/api/common/types/oauth';
import { cookieExpiresAt, createSessionTokenCookie, setSessionCookie } from '$lib/server/api/common/utils/cookies';
import { SessionsService } from '$lib/server/api/iam/sessions/sessions.service';
import { OAuthService } from '$lib/server/api/oauth/oauth.service';
import { github, google } from '$lib/server/auth';
import { inject, injectable } from '@needle-di/core';
import { OAuth2RequestError } from 'arctic';
import { getCookie } from 'hono/cookie';
@injectable()
export class OAuthController extends Controller {
constructor(
private oauthService = inject(OAuthService),
private sessionsService = inject(SessionsService),
) {
super();
}
routes() {
return this.controller
.get('/github', async (c) => {
try {
const code = c.req.query('code')?.toString() ?? null;
const state = c.req.query('state')?.toString() ?? null;
const storedState = getCookie(c).github_oauth_state ?? null;
if (!code || !state || !storedState || state !== storedState) {
return c.body(null, 400);
}
const tokens = await github.validateAuthorizationCode(code);
const githubUserResponse = await fetch('https://api.github.com/user', {
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
},
});
const githubUser: GitHubUser = await githubUserResponse.json();
const oAuthUser: OAuthUser = {
sub: `${githubUser.id}`,
username: githubUser.login,
email: undefined,
};
const userId = await this.oauthService.handleOAuthUser(oAuthUser, 'github');
const sessionToken = this.sessionsService.generateSessionToken();
const session = await this.sessionsService.createSession(sessionToken, userId, '', '', false, false);
const sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt);
setSessionCookie(c, sessionCookie);
return c.json({ message: 'ok' });
} catch (error) {
console.error(error);
// the specific error message depends on the provider
if (error instanceof OAuth2RequestError) {
// invalid code
return c.body(null, 400);
}
return c.body(null, 500);
}
})
.get('/google', async (c) => {
try {
const code = c.req.query('code')?.toString() ?? null;
const state = c.req.query('state')?.toString() ?? null;
const storedState = getCookie(c).google_oauth_state ?? null;
const storedCodeVerifier = getCookie(c).google_oauth_code_verifier ?? null;
if (!code || !storedState || !storedCodeVerifier || state !== storedState) {
return c.body(null, 400);
}
const tokens = await google.validateAuthorizationCode(code, storedCodeVerifier);
const googleUserResponse = await fetch('https://openidconnect.googleapis.com/v1/userinfo', {
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
},
});
const googleUser: GoogleUser = await googleUserResponse.json();
const oAuthUser: OAuthUser = {
sub: googleUser.sub,
given_name: googleUser.given_name,
family_name: googleUser.family_name,
picture: googleUser.picture,
username: googleUser.email,
email: googleUser.email,
email_verified: googleUser.email_verified,
};
const userId = await this.oauthService.handleOAuthUser(oAuthUser, 'google');
const sessionToken = this.sessionsService.generateSessionToken();
const session = await this.sessionsService.createSession(sessionToken, userId, '', '', false, false);
const sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt);
setSessionCookie(c, sessionCookie);
return c.json({ message: 'ok' });
} catch (error) {
console.error(error);
// the specific error message depends on the provider
if (error instanceof OAuth2RequestError) {
// invalid code
return c.body(null, 400);
}
return c.body(null, 500);
}
});
}
}
interface GitHubUser {
id: number;
login: string;
}
interface GoogleUser {
sub: string;
name: string;
given_name: string;
family_name: string;
picture: string;
email: string;
email_verified: boolean;
}

View file

@ -0,0 +1,27 @@
import type { OAuthProviders, OAuthUser } from '$lib/server/api/common/types/oauth';
import { inject, injectable } from '@needle-di/core';
import { FederatedIdentityRepository } from '../users/federated_identity.repository';
import { UsersService } from '../users/users.service';
@injectable()
export class OAuthService {
constructor(
private federatedIdentityRepository = inject(FederatedIdentityRepository),
private usersService = inject(UsersService),
) {}
async handleOAuthUser(oAuthUser: OAuthUser, oauthProvider: OAuthProviders) {
const federatedUser = await this.federatedIdentityRepository.findOneByFederatedUserIdAndProvider(oAuthUser.sub, oauthProvider);
if (federatedUser) {
return federatedUser.user_id;
}
const user = await this.usersService.createOAuthUser(oAuthUser, oauthProvider);
if (!user) {
throw new Error('Failed to create user');
}
return user.id;
}
}

View file

@ -1,83 +0,0 @@
import {takeFirstOrThrow} from '$lib/server/api/common/utils/repository';
import {DrizzleService} from '$lib/server/api/services/drizzle.service';
import {eq, type InferInsertModel} from 'drizzle-orm';
import {inject, injectable} from '@needle-di/core';
import {collections} from '../databases/postgres/tables';
export type CreateCollection = InferInsertModel<typeof collections>;
export type UpdateCollection = Partial<CreateCollection>;
@injectable()
export class CollectionsRepository {
constructor(private drizzle = inject(DrizzleService)) {}
async findAll(db = this.drizzle.db) {
return db.query.collections.findMany();
}
async findOneById(id: string, db = this.drizzle.db) {
return db.query.collections.findFirst({
where: eq(collections.id, id),
columns: {
cuid: true,
name: true,
},
});
}
async findOneByCuid(cuid: string, db = this.drizzle.db) {
return db.query.collections.findFirst({
where: eq(collections.cuid, cuid),
columns: {
cuid: true,
name: true,
},
});
}
async findOneByUserId(userId: string, db = this.drizzle.db) {
return db.query.collections.findFirst({
where: eq(collections.user_id, userId),
columns: {
cuid: true,
name: true,
},
});
}
async findAllByUserId(userId: string, db = this.drizzle.db) {
return db.query.collections.findMany({
where: eq(collections.user_id, userId),
columns: {
cuid: true,
name: true,
createdAt: true,
},
});
}
async findAllByUserIdWithDetails(userId: string, db = this.drizzle.db) {
return db.query.collections.findMany({
where: eq(collections.user_id, userId),
columns: {
cuid: true,
name: true,
},
with: {
collection_items: {
columns: {
cuid: true,
},
},
},
});
}
async create(data: CreateCollection, db = this.drizzle.db) {
return db.insert(collections).values(data).returning().then(takeFirstOrThrow);
}
async update(id: string, data: UpdateCollection, db = this.drizzle.db) {
return db.update(collections).set(data).where(eq(collections.id, id)).returning().then(takeFirstOrThrow);
}
}

View file

@ -1,70 +0,0 @@
import {credentialsTable, CredentialsType} from '$lib/server/api/databases/postgres/tables/credentials.table';
import {DrizzleService} from '$lib/server/api/services/drizzle.service';
import {and, eq, type InferInsertModel} from 'drizzle-orm';
import {inject, injectable} from '@needle-di/core';
import {takeFirstOrThrow} from '../common/utils/repository';
export type CreateCredentials = InferInsertModel<typeof credentialsTable>;
export type UpdateCredentials = Partial<CreateCredentials>;
export type DeleteCredentials = Pick<CreateCredentials, 'id'>;
@injectable()
export class CredentialsRepository {
constructor(private drizzle = inject(DrizzleService)) {}
async findOneByUserId(userId: string, db = this.drizzle.db) {
return db.query.credentialsTable.findFirst({
where: eq(credentialsTable.user_id, userId),
});
}
async findOneByUserIdAndType(userId: string, type: CredentialsType, db = this.drizzle.db) {
return db.query.credentialsTable.findFirst({
where: and(eq(credentialsTable.user_id, userId), eq(credentialsTable.type, type)),
});
}
async findPasswordCredentialsByUserId(userId: string, db = this.drizzle.db) {
return db.query.credentialsTable.findFirst({
where: and(eq(credentialsTable.user_id, userId), eq(credentialsTable.type, CredentialsType.PASSWORD)),
});
}
async findTOTPCredentialsByUserId(userId: string, db = this.drizzle.db) {
return db.query.credentialsTable.findFirst({
where: and(eq(credentialsTable.user_id, userId), eq(credentialsTable.type, CredentialsType.TOTP)),
});
}
async findOneById(id: string, db = this.drizzle.db) {
return db.query.credentialsTable.findFirst({
where: eq(credentialsTable.id, id),
});
}
async findOneByIdOrThrow(id: string, db = this.drizzle.db) {
const credentials = await this.findOneById(id, db);
if (!credentials) throw Error('Credentials not found');
return credentials;
}
async create(data: CreateCredentials, db = this.drizzle.db) {
return db.insert(credentialsTable).values(data).returning().then(takeFirstOrThrow);
}
async update(id: string, data: UpdateCredentials, db = this.drizzle.db) {
return db.update(credentialsTable).set(data).where(eq(credentialsTable.id, id)).returning().then(takeFirstOrThrow);
}
async delete(id: string, db = this.drizzle.db) {
return db.delete(credentialsTable).where(eq(credentialsTable.id, id));
}
async deleteByUserId(userId: string, db = this.drizzle.db) {
return db.delete(credentialsTable).where(eq(credentialsTable.user_id, userId));
}
async deleteByUserIdAndType(userId: string, type: CredentialsType, db = this.drizzle.db) {
return db.delete(credentialsTable).where(and(eq(credentialsTable.user_id, userId), eq(credentialsTable.type, type)));
}
}

View file

@ -1,28 +0,0 @@
import {and, eq, type InferInsertModel} from 'drizzle-orm';
import {inject, injectable} from '@needle-di/core';
import {takeFirstOrThrow} from '../common/utils/repository';
import {federatedIdentityTable} from '../databases/postgres/tables';
import {DrizzleService} from '../services/drizzle.service';
export type CreateFederatedIdentity = InferInsertModel<typeof federatedIdentityTable>;
@injectable()
export class FederatedIdentityRepository {
constructor(private drizzle = inject(DrizzleService)) {}
async findOneByUserIdAndProvider(userId: string, provider: string) {
return this.drizzle.db.query.federatedIdentityTable.findFirst({
where: and(eq(federatedIdentityTable.user_id, userId), eq(federatedIdentityTable.identity_provider, provider)),
});
}
async findOneByFederatedUserIdAndProvider(federatedUserId: string, provider: string) {
return this.drizzle.db.query.federatedIdentityTable.findFirst({
where: and(eq(federatedIdentityTable.federated_user_id, federatedUserId), eq(federatedIdentityTable.identity_provider, provider)),
});
}
async create(data: CreateFederatedIdentity, db = this.drizzle.db) {
return db.insert(federatedIdentityTable).values(data).returning().then(takeFirstOrThrow);
}
}

View file

@ -1,32 +0,0 @@
import {takeFirstOrThrow} from '$lib/server/api/common/utils/repository';
import {DrizzleService} from '$lib/server/api/services/drizzle.service';
import {and, eq, type InferInsertModel} from 'drizzle-orm';
import {inject, injectable} from '@needle-di/core';
import {recoveryCodesTable} from '../databases/postgres/tables';
export type CreateRecoveryCodes = InferInsertModel<typeof recoveryCodesTable>;
@injectable()
export class RecoveryCodesRepository {
constructor(private drizzle = inject(DrizzleService)) {}
async create(data: CreateRecoveryCodes, db = this.drizzle.db) {
return db.insert(recoveryCodesTable).values(data).returning().then(takeFirstOrThrow);
}
async findAllByUserId(userId: string, db = this.drizzle.db) {
return db.query.recoveryCodesTable.findMany({
where: eq(recoveryCodesTable.userId, userId),
});
}
async findAllNotUsedByUserId(userId: string, db = this.drizzle.db) {
return db.query.recoveryCodesTable.findMany({
where: and(eq(recoveryCodesTable.userId, userId), eq(recoveryCodesTable.used, false)),
});
}
async deleteAllByUserId(userId: string, db = this.drizzle.db) {
return db.delete(recoveryCodesTable).where(eq(recoveryCodesTable.userId, userId));
}
}

View file

@ -1,53 +0,0 @@
import {DrizzleService} from '$lib/server/api/services/drizzle.service';
import {eq, type InferInsertModel} from 'drizzle-orm';
import {inject, injectable} from '@needle-di/core';
import {takeFirstOrThrow} from '../common/utils/repository';
import {rolesTable} from '../databases/postgres/tables';
export type CreateRole = InferInsertModel<typeof rolesTable>;
export type UpdateRole = Partial<CreateRole>;
@injectable()
export class RolesRepository {
constructor(private drizzle = inject(DrizzleService)) {}
async findOneById(id: string, db = this.drizzle.db) {
return db.query.rolesTable.findFirst({
where: eq(rolesTable.id, id),
});
}
async findOneByIdOrThrow(id: string, db = this.drizzle.db) {
const role = await this.findOneById(id, db);
if (!role) throw Error('Role not found');
return role;
}
async findAll(db = this.drizzle.db) {
return db.query.rolesTable.findMany();
}
async findOneByName(name: string, db = this.drizzle.db) {
return db.query.rolesTable.findFirst({
where: eq(rolesTable.name, name),
});
}
async findOneByNameOrThrow(name: string, db = this.drizzle.db) {
const role = await this.findOneByName(name, db);
if (!role) throw Error('Role not found');
return role;
}
async create(data: CreateRole, db = this.drizzle.db) {
return db.insert(rolesTable).values(data).returning().then(takeFirstOrThrow);
}
async update(id: string, data: UpdateRole, db = this.drizzle.db) {
return db.update(rolesTable).set(data).where(eq(rolesTable.id, id)).returning().then(takeFirstOrThrow);
}
async delete(id: string, db = this.drizzle.db) {
return db.delete(rolesTable).where(eq(rolesTable.id, id)).returning().then(takeFirstOrThrow);
}
}

View file

@ -1,32 +0,0 @@
import {takeFirstOrThrow} from '$lib/server/api/common/utils/repository';
import {DrizzleService} from '$lib/server/api/services/drizzle.service';
import {eq, type InferInsertModel} from 'drizzle-orm';
import {inject, injectable} from '@needle-di/core';
import {sessionsTable, usersTable} from '../databases/postgres/tables';
export type CreateSession = InferInsertModel<typeof sessionsTable>;
@injectable()
export class SessionsRepository {
constructor(private drizzle = inject(DrizzleService)) {}
async create(data: CreateSession, db = this.drizzle.db) {
return db.insert(sessionsTable).values(data).returning().then(takeFirstOrThrow);
}
async findBySessionId(sessionId: string, db = this.drizzle.db) {
return await db
.select({ user: usersTable, session: sessionsTable })
.from(sessionsTable)
.innerJoin(usersTable, eq(sessionsTable.userId, usersTable.id))
.where(eq(sessionsTable.id, sessionId));
}
async deleteBySessionId(sessionId: string, db = this.drizzle.db) {
return db.delete(sessionsTable).where(eq(sessionsTable.id, sessionId));
}
async updateSessionExpiresAt(sessionId: string, expiresAt: Date, db = this.drizzle.db) {
db.update(sessionsTable).set({ expiresAt }).where(eq(sessionsTable.id, sessionId));
}
}

View file

@ -1,39 +0,0 @@
import {DrizzleService} from '$lib/server/api/services/drizzle.service';
import {eq, type InferInsertModel} from 'drizzle-orm';
import {inject, injectable} from '@needle-di/core';
import {takeFirstOrThrow} from '../common/utils/repository';
import {user_roles} from '../databases/postgres/tables';
export type CreateUserRole = InferInsertModel<typeof user_roles>;
export type UpdateUserRole = Partial<CreateUserRole>;
@injectable()
export class UserRolesRepository {
constructor(private drizzle = inject(DrizzleService)) {}
async findOneById(id: string, db = this.drizzle.db) {
return db.query.user_roles.findFirst({
where: eq(user_roles.id, id),
});
}
async findOneByIdOrThrow(id: string, db = this.drizzle.db) {
const userRole = await this.findOneById(id, db);
if (!userRole) throw Error('User not found');
return userRole;
}
async findAllByUserId(userId: string, db = this.drizzle.db) {
return db.query.user_roles.findMany({
where: eq(user_roles.user_id, userId),
});
}
async create(data: CreateUserRole, db = this.drizzle.db) {
return db.insert(user_roles).values(data).returning().then(takeFirstOrThrow);
}
async delete(id: string, db = this.drizzle.db) {
return db.delete(user_roles).where(eq(user_roles.id, id)).returning().then(takeFirstOrThrow);
}
}

View file

@ -1,49 +0,0 @@
import {usersTable} from '$lib/server/api/databases/postgres/tables/users.table';
import {DrizzleService} from '$lib/server/api/services/drizzle.service';
import {eq, type InferInsertModel} from 'drizzle-orm';
import {inject, injectable} from '@needle-di/core';
import {takeFirstOrThrow} from '../common/utils/repository';
export type CreateUser = InferInsertModel<typeof usersTable>;
export type UpdateUser = Partial<CreateUser>;
@injectable()
export class UsersRepository {
constructor(private drizzle = inject(DrizzleService)) {}
async findOneById(id: string, db = this.drizzle.db) {
return db.query.usersTable.findFirst({
where: eq(usersTable.id, id),
});
}
async findOneByIdOrThrow(id: string, db = this.drizzle.db) {
const user = await this.findOneById(id);
if (!user) throw Error('User not found');
return user;
}
async findOneByUsername(username: string, db = this.drizzle.db) {
return db.query.usersTable.findFirst({
where: eq(usersTable.username, username),
});
}
async findOneByEmail(email: string, db = this.drizzle.db) {
return db.query.usersTable.findFirst({
where: eq(usersTable.email, email),
});
}
async create(data: CreateUser, db = this.drizzle.db) {
return db.insert(usersTable).values(data).returning().then(takeFirstOrThrow);
}
async update(id: string, data: UpdateUser, db = this.drizzle.db) {
return db.update(usersTable).set(data).where(eq(usersTable.id, id)).returning().then(takeFirstOrThrow);
}
async delete(id: string, db = this.drizzle.db) {
return db.delete(usersTable).where(eq(usersTable.id, id)).returning().then(takeFirstOrThrow);
}
}

View file

@ -1,66 +0,0 @@
import {DrizzleService} from '$lib/server/api/services/drizzle.service';
import {eq, type InferInsertModel} from 'drizzle-orm';
import {inject, injectable} from '@needle-di/core';
import {takeFirstOrThrow} from '../common/utils/repository';
import {wishlistsTable} from '../databases/postgres/tables';
export type CreateWishlist = InferInsertModel<typeof wishlistsTable>;
export type UpdateWishlist = Partial<CreateWishlist>;
@injectable()
export class WishlistsRepository {
constructor(private drizzle = inject(DrizzleService)) {}
async findAll(db = this.drizzle.db) {
return db.query.wishlistsTable.findMany();
}
async findOneById(id: string, db = this.drizzle.db) {
return db.query.wishlistsTable.findFirst({
where: eq(wishlistsTable.id, id),
columns: {
cuid: true,
name: true,
},
});
}
async findOneByCuid(cuid: string, db = this.drizzle.db) {
return db.query.wishlistsTable.findFirst({
where: eq(wishlistsTable.cuid, cuid),
columns: {
cuid: true,
name: true,
},
});
}
async findOneByUserId(userId: string, db = this.drizzle.db) {
return db.query.wishlistsTable.findFirst({
where: eq(wishlistsTable.user_id, userId),
columns: {
cuid: true,
name: true,
},
});
}
async findAllByUserId(userId: string, db = this.drizzle.db) {
return db.query.wishlistsTable.findMany({
where: eq(wishlistsTable.user_id, userId),
columns: {
cuid: true,
name: true,
createdAt: true,
},
});
}
async create(data: CreateWishlist, db = this.drizzle.db) {
return db.insert(wishlistsTable).values(data).returning().then(takeFirstOrThrow);
}
async update(id: string, data: UpdateWishlist, db = this.drizzle.db) {
return db.update(wishlistsTable).set(data).where(eq(wishlistsTable.id, id)).returning().then(takeFirstOrThrow);
}
}

View file

@ -1,50 +0,0 @@
import type {db} from '$lib/server/api/packages/drizzle'
import {generateRandomAnimalName} from '$lib/utils/randomDataUtil'
import {inject, injectable} from '@needle-di/core'
import {CollectionsRepository} from '../repositories/collections.repository'
@injectable()
export class CollectionsService {
constructor(private collectionsRepository = inject(CollectionsRepository)) {}
async findOneByUserId(userId: string) {
return this.collectionsRepository.findOneByUserId(userId)
}
async findAllByUserId(userId: string) {
return this.collectionsRepository.findAllByUserId(userId)
}
async findAllByUserIdWithDetails(userId: string) {
return this.collectionsRepository.findAllByUserIdWithDetails(userId)
}
async findOneById(id: string) {
return this.collectionsRepository.findOneById(id)
}
async findOneByCuid(cuid: string) {
return this.collectionsRepository.findOneByCuid(cuid)
}
async createEmptyNoName(userId: string, trx: Parameters<Parameters<typeof db.transaction>[0]>[0] | null = null) {
return this.createEmpty(userId, null, trx)
}
async createEmpty(userId: string, name: string | null, trx: Parameters<Parameters<typeof db.transaction>[0]>[0] | null = null) {
if (!trx) {
return this.collectionsRepository.create({
user_id: userId,
name: name ?? generateRandomAnimalName(),
})
}
return this.collectionsRepository.create(
{
user_id: userId,
name: name ?? generateRandomAnimalName(),
},
trx,
)
}
}

View file

@ -1,35 +0,0 @@
import {drizzle, type NodePgDatabase} from 'drizzle-orm/node-postgres';
import pg from 'pg';
import {type Disposable, injectable} from '@needle-di/core';
import {config} from '../common/config';
import * as schema from '../databases/postgres/tables';
@injectable()
export class DrizzleService implements Disposable {
protected readonly pool: pg.Pool;
db: NodePgDatabase<typeof schema>;
readonly schema: typeof schema = schema;
constructor() {
const pool = new pg.Pool({
user: config.postgres.user,
password: config.postgres.password,
host: config.postgres.host,
port: Number(config.postgres.port).valueOf(),
database: config.postgres.database,
ssl: config.postgres.ssl,
max: config.postgres.max,
});
this.pool = pool;
this.db = drizzle({
client: pool,
casing: 'snake_case',
schema,
logger: !config.isProduction,
});
}
dispose(): Promise<void> | void {
this.pool.end();
}
}

View file

@ -1,51 +0,0 @@
import {scrypt} from 'node:crypto'
import {decodeHex, encodeHexLowerCase} from '@oslojs/encoding'
import {constantTimeEqual} from '@oslojs/crypto/subtle'
import {injectable} from '@needle-di/core'
@injectable()
export class HashingService {
private N: number
private r: number
private p: number
private dkLen: number
constructor() {
this.N = 16384
this.r = 16
this.p = 1
this.dkLen = 64
}
async hash(password: string) {
const salt = encodeHexLowerCase(crypto.getRandomValues(new Uint8Array(16)))
const key = await this.generateKey(password, salt)
return `${salt}:${encodeHexLowerCase(key)}`
}
async verify(hash: string, password: string) {
const [salt, key] = hash.split(':')
const targetKey = await this.generateKey(password, salt)
return constantTimeEqual(targetKey, decodeHex(key))
}
async generateKey(password: string, salt: string): Promise<Buffer> {
return await new Promise((resolve, reject) => {
scrypt(
password.normalize('NFKC'),
salt,
this.dkLen,
{
N: this.N,
p: this.p,
r: this.r,
// errors when 128 * N * r > `maxmem` (approximately)
maxmem: 128 * this.N * this.r * 2,
},
(err, buff) => {
if (err) return reject(err)
return resolve(buff)
},
)
})
}
}

View file

@ -1,27 +0,0 @@
import {inject, injectable} from '@needle-di/core'
import {FederatedIdentityRepository} from '../repositories/federated_identity.repository'
import {UsersService} from './users.service'
import type {OAuthProviders, OAuthUser} from "$lib/server/api/common/types/oauth";
@injectable()
export class OAuthService {
constructor(
private federatedIdentityRepository = inject(FederatedIdentityRepository),
private usersService = inject(UsersService),
) {}
async handleOAuthUser(oAuthUser: OAuthUser, oauthProvider: OAuthProviders) {
const federatedUser = await this.federatedIdentityRepository.findOneByFederatedUserIdAndProvider(oAuthUser.sub, oauthProvider)
if (federatedUser) {
return federatedUser.user_id
}
const user = await this.usersService.createOAuthUser(oAuthUser, oauthProvider)
if (!user) {
throw new Error('Failed to create user')
}
return user.id
}
}

View file

@ -1,40 +0,0 @@
import {RecoveryCodesRepository} from '$lib/server/api/repositories/recovery-codes.repository'
import {alphabet, generateRandomString} from 'oslo/crypto'
import {inject, injectable} from '@needle-di/core'
import {HashingService} from './hashing.service'
@injectable()
export class RecoveryCodesService {
constructor(
private hashingService = inject(HashingService),
private recoveryCodesRepository = inject(RecoveryCodesRepository),
) {}
async findAllRecoveryCodesByUserId(userId: string) {
return this.recoveryCodesRepository.findAllByUserId(userId)
}
async createRecoveryCodes(userId: string) {
const createdRecoveryCodes = Array.from({ length: 5 }, () => generateRandomString(10, alphabet('A-Z', '0-9')))
if (createdRecoveryCodes && userId) {
for (const code of createdRecoveryCodes) {
const hashedCode = await this.hashingService.hash(code)
console.log('Inserting recovery code', code, hashedCode)
await this.recoveryCodesRepository.create({ userId, code: hashedCode })
}
return createdRecoveryCodes
}
return []
}
async verify(userId: string, code: string) {
const recoveryCodes = await this.recoveryCodesRepository.findAllNotUsedByUserId(userId);
return recoveryCodes.find(recoveryCode => this.hashingService.verify(recoveryCode.code, code))
}
async deleteAllRecoveryCodesByUserId(userId: string) {
return this.recoveryCodesRepository.deleteAllByUserId(userId)
}
}

View file

@ -1,40 +0,0 @@
import {config} from '$lib/server/api/common/config'
import {Redis} from 'ioredis'
import { injectable} from '@needle-di/core';
import type {Disposable} from 'tsyringe';
@injectable()
export class RedisService implements Disposable {
readonly client: Redis;
constructor() {
this.client = new Redis(config.redis.url, {
maxRetriesPerRequest: null,
});
}
async get(data: { prefix: string; key: string }): Promise<string | null> {
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

@ -1,14 +0,0 @@
import {inject, injectable} from "@needle-di/core";
import {RolesRepository} from "$lib/server/api/repositories/roles.repository";
@injectable()
export class RolesService {
constructor(
private rolesRepository = inject(RolesRepository)
) { }
async findOneByNameOrThrow(name: string) {
return this.rolesRepository.findOneByNameOrThrow(name);
}
}

View file

@ -1,135 +0,0 @@
import { cookieExpiresAt, cookieExpiresMilliseconds, halfCookieExpiresMilliseconds } from '$lib/server/api/common/utils/cookies';
import { UsersRepository } from '$lib/server/api/repositories/users.repository';
import { RedisService } from '$lib/server/api/services/redis.service';
import { sha256 } from '@oslojs/crypto/sha2';
import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding';
import { inject, injectable } from '@needle-di/core';
import type { Users } from '../databases/postgres/tables';
export type RedisSession = {
id: string;
user_id: string;
expires_at: number;
ip_country: string;
ip_address: string;
two_factor_auth_enabled: boolean;
is_two_factor_authenticated: boolean;
};
export type Session = {
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),
) {}
generateSessionToken() {
const bytes = new Uint8Array(20);
crypto.getRandomValues(bytes);
return encodeBase32LowerCaseNoPadding(bytes);
}
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<SessionValidationResult> {
// 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_enabled: session.twoFactorEnabled,
two_factor_verified: session.twoFactorVerified,
}),
'EXAT',
Math.floor(session.expiresAt.getTime() / 1000),
);
}
return { session, user };
}
async invalidateSession(sessionId: string) {
await this.redisService.client.del(`session:${sessionId}`);
}
}

View file

@ -1,11 +1,11 @@
import {inject, injectable} from "@needle-di/core"; import { inject, injectable } from '@needle-di/core';
import {generateRandomString} from "oslo/crypto"; import { TimeSpan, type TimeSpanUnit, createDate } from 'oslo';
import {createDate, TimeSpan, type TimeSpanUnit} from 'oslo'; import { generateRandomString } from 'oslo/crypto';
import {HashingService} from "./hashing.service"; import { HashingService } from '../common/services/hashing.service';
@injectable() @injectable()
export class TokensService { export class TokensService {
constructor(private hashingService = inject(HashingService)) { } constructor(private hashingService = inject(HashingService)) {}
generateToken() { generateToken() {
const alphabet = '23456789ACDEFGHJKLMNPQRSTUVWXYZ'; // alphabet with removed look-alike characters (0, 1, O, I) const alphabet = '23456789ACDEFGHJKLMNPQRSTUVWXYZ'; // alphabet with removed look-alike characters (0, 1, O, I)
@ -15,25 +15,25 @@ export class TokensService {
generateTokenWithExpiry(number: number, lifespan: TimeSpanUnit) { generateTokenWithExpiry(number: number, lifespan: TimeSpanUnit) {
return { return {
token: this.generateToken(), token: this.generateToken(),
expiry: createDate(new TimeSpan(number, lifespan)) expiry: createDate(new TimeSpan(number, lifespan)),
} };
} }
async generateTokenWithExpiryAndHash(number: number, lifespan: TimeSpanUnit) { async generateTokenWithExpiryAndHash(number: number, lifespan: TimeSpanUnit) {
const token = this.generateToken() const token = this.generateToken();
const hashedToken = await this.hashingService.hash(token) const hashedToken = await this.hashingService.hash(token);
return { return {
token, token,
hashedToken, hashedToken,
expiry: createDate(new TimeSpan(number, lifespan)) expiry: createDate(new TimeSpan(number, lifespan)),
} };
} }
async createHashedToken(token: string) { async createHashedToken(token: string) {
return this.hashingService.hash(token) return this.hashingService.hash(token);
} }
async verifyHashedToken(hashedToken: string, token: string) { async verifyHashedToken(hashedToken: string, token: string) {
return this.hashingService.verify(hashedToken, token) return this.hashingService.verify(hashedToken, token);
} }
} }

View file

@ -1,51 +0,0 @@
import type {db} from '$lib/server/api/packages/drizzle'
import {type CreateUserRole, UserRolesRepository} from '$lib/server/api/repositories/user_roles.repository'
import {RolesService} from '$lib/server/api/services/roles.service'
import {inject, injectable} from '@needle-di/core'
@injectable()
export class UserRolesService {
constructor(
private userRolesRepository = inject(UserRolesRepository),
private rolesService = inject(RolesService),
) {}
async findOneById(id: string) {
return this.userRolesRepository.findOneById(id)
}
async findAllByUserId(userId: string) {
return this.userRolesRepository.findAllByUserId(userId)
}
async create(data: CreateUserRole) {
return this.userRolesRepository.create(data)
}
async addRoleToUser(userId: string, roleName: string, primary = false, trx: Parameters<Parameters<typeof db.transaction>[0]>[0] | null = null) {
// Find the role by its name
const role = await this.rolesService.findOneByNameOrThrow(roleName)
if (!role || !role.id) {
throw new Error(`Role with name ${roleName} not found`)
}
if (!trx) {
return this.userRolesRepository.create({
user_id: userId,
role_id: role.id,
primary,
})
}
// Create a UserRole entry linking the user and the role
return this.userRolesRepository.create(
{
user_id: userId,
role_id: role.id,
primary,
},
trx,
)
}
}

View file

@ -1,150 +0,0 @@
import type {OAuthUser} from '$lib/server/api/common/types/oauth';
import type {SignupUsernameEmailDto} from '$lib/server/api/dtos/signup-username-email.dto';
import {CredentialsRepository} from '$lib/server/api/repositories/credentials.repository';
import {FederatedIdentityRepository} from '$lib/server/api/repositories/federated_identity.repository';
import {WishlistsRepository} from '$lib/server/api/repositories/wishlists.repository';
import {TokensService} from '$lib/server/api/services/tokens.service';
import {UserRolesService} from '$lib/server/api/services/user_roles.service';
import { inject, injectable } from '@needle-di/core';
import {CredentialsType, RoleName} from '../databases/postgres/tables';
import {type UpdateUser, UsersRepository} from '../repositories/users.repository';
import {CollectionsService} from './collections.service';
import {DrizzleService} from './drizzle.service';
import {WishlistsService} from './wishlists.service';
@injectable()
export class UsersService {
constructor(
private collectionsService = inject(CollectionsService),
private credentialsRepository = inject(CredentialsRepository),
private drizzleService = inject(DrizzleService),
private federatedIdentityRepository = inject(FederatedIdentityRepository),
private tokenService = inject(TokensService),
private usersRepository = inject(UsersRepository),
private userRolesService = inject(UserRolesService),
private wishlistsRepository = inject(WishlistsRepository),
private wishlistsService = inject(WishlistsService),
) {}
async create(data: SignupUsernameEmailDto) {
const { firstName, lastName, email, username, password } = data;
const hashedPassword = await this.tokenService.createHashedToken(password);
return await this.drizzleService.db.transaction(async (trx) => {
const createdUser = await this.usersRepository.create(
{
first_name: firstName,
last_name: lastName,
email,
username,
},
trx,
);
if (!createdUser) {
return null;
}
const credentials = await this.credentialsRepository.create(
{
user_id: createdUser.id,
type: CredentialsType.PASSWORD,
secret_data: hashedPassword,
},
trx,
);
if (!credentials) {
await this.usersRepository.delete(createdUser.id);
return null;
}
await this.userRolesService.addRoleToUser(createdUser.id, RoleName.USER, true, trx);
await this.wishlistsService.createEmptyNoName(createdUser.id, trx);
await this.collectionsService.createEmptyNoName(createdUser.id, trx);
return createdUser;
});
}
async createOAuthUser(oAuthUser: OAuthUser, oauthProvider: string) {
return await this.drizzleService.db.transaction(async (trx) => {
const createdUser = await this.usersRepository.create(
{
username: oAuthUser.username || oAuthUser.username,
email: oAuthUser.email || null,
first_name: oAuthUser.given_name || null,
last_name: oAuthUser.family_name || null,
picture: oAuthUser.picture || null,
email_verified: oAuthUser.email_verified || false,
},
trx,
);
if (!createdUser) {
return null;
}
await this.federatedIdentityRepository.create(
{
identity_provider: oauthProvider,
user_id: createdUser.id,
federated_user_id: oAuthUser.sub,
federated_username: oAuthUser.email || oAuthUser.username,
},
trx,
);
await this.userRolesService.addRoleToUser(createdUser.id, RoleName.USER, true, trx);
await this.wishlistsService.createEmptyNoName(createdUser.id, trx);
await this.collectionsService.createEmptyNoName(createdUser.id, trx);
return createdUser;
});
}
async updateUser(userId: string, data: UpdateUser) {
return this.usersRepository.update(userId, data);
}
async findOneByUsername(username: string) {
return this.usersRepository.findOneByUsername(username);
}
async findOneByEmail(email: string) {
return this.usersRepository.findOneByEmail(email);
}
async findOneById(id: string) {
return this.usersRepository.findOneById(id);
}
async updatePassword(userId: string, password: string) {
const hashedPassword = await this.tokenService.createHashedToken(password);
const currentCredentials = await this.credentialsRepository.findPasswordCredentialsByUserId(userId);
if (!currentCredentials) {
await this.credentialsRepository.create({
user_id: userId,
type: CredentialsType.PASSWORD,
secret_data: hashedPassword,
});
} else {
await this.credentialsRepository.update(currentCredentials.id, {
secret_data: hashedPassword,
});
}
}
async verifyPassword(userId: string, data: { password: string }) {
const user = await this.usersRepository.findOneById(userId);
if (!user) {
throw new Error('User not found');
}
const credential = await this.credentialsRepository.findOneByUserIdAndType(userId, CredentialsType.PASSWORD);
if (!credential) {
throw new Error('Password credentials not found');
}
const { password } = data;
return this.tokenService.verifyHashedToken(credential.secret_data, password);
}
}

View file

@ -1,41 +0,0 @@
import type {db} from '$lib/server/api/packages/drizzle'
import {generateRandomAnimalName} from '$lib/utils/randomDataUtil'
import {inject, injectable} from '@needle-di/core'
import {WishlistsRepository} from '../repositories/wishlists.repository'
@injectable()
export class WishlistsService {
constructor(private wishlistsRepository = inject(WishlistsRepository)) {}
async findAllByUserId(userId: string) {
return this.wishlistsRepository.findAllByUserId(userId)
}
async findOneById(id: string) {
return this.wishlistsRepository.findOneById(id)
}
async findOneByCuid(cuid: string) {
return this.wishlistsRepository.findOneByCuid(cuid)
}
async createEmptyNoName(userId: string, trx: Parameters<Parameters<typeof db.transaction>[0]>[0] | null = null) {
return this.createEmpty(userId, null, trx)
}
async createEmpty(userId: string, name: string | null, trx: Parameters<Parameters<typeof db.transaction>[0]>[0] | null = null) {
if (!trx) {
return this.wishlistsRepository.create({
user_id: userId,
name: name ?? generateRandomAnimalName(),
})
}
return this.wishlistsRepository.create(
{
user_id: userId,
name: name ?? generateRandomAnimalName(),
},
trx,
)
}
}

View file

@ -0,0 +1,44 @@
import 'reflect-metadata';
import { limiter } from '$lib/server/api/common/middleware/rate-limit.middleware';
import { Controller } from '$lib/server/api/common/types/controller';
import { cookieExpiresAt, createSessionTokenCookie, setSessionCookie } from '$lib/server/api/common/utils/cookies';
import { signupUsernameEmailDto } from '$lib/server/api/dtos/signup-username-email.dto';
import { SessionsService } from '$lib/server/api/iam/sessions/sessions.service';
import { LoginRequestsService } from '$lib/server/api/login/loginrequest.service';
import { UsersService } from '$lib/server/api/users/users.service';
import { zValidator } from '@hono/zod-validator';
import { inject, injectable } from '@needle-di/core';
@injectable()
export class SignupController extends Controller {
constructor(
private usersService = inject(UsersService),
private loginRequestService = inject(LoginRequestsService),
private sessionsService = inject(SessionsService),
) {
super();
}
routes() {
return this.controller.post('/', zValidator('json', signupUsernameEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
const { firstName, lastName, email, username, password, confirm_password } = await c.req.valid('json');
const existingUser = await this.usersService.findOneByUsername(username);
if (existingUser) {
return c.body('User already exists', 400);
}
const user = await this.usersService.create({ firstName, lastName, email, username, password, confirm_password });
if (!user) {
return c.body('Failed to create user', 500);
}
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);
return c.json({ message: 'ok' });
});
}
}

View file

@ -1,38 +0,0 @@
import { Container } from '@needle-di/core';
import {afterAll, beforeAll, describe, expect, it, vi} from 'vitest'
import {HashingService} from '../services/hashing.service'
describe('HashingService', () => {
let service: HashingService
const container = new Container()
beforeAll(() => {
service = container.get(HashingService)
})
afterAll(() => {
vi.resetAllMocks()
})
describe('Create Hash', () => {
it('should create a hash', async () => {
const hash = await service.hash('111')
expect(hash).not.toBeUndefined()
expect(hash).not.toBeNull()
})
})
describe('Verify Hash', () => {
it('should verify a hash', async () => {
const hash = await service.hash('111')
const verifiable = await service.verify(hash, '111')
expect(verifiable).toBeTruthy()
})
it('should not verify a hash', async () => {
const hash = await service.hash('111')
const verifiable = await service.verify(hash, '222')
expect(verifiable).toBeFalsy()
})
})
})

View file

@ -1,9 +1,9 @@
import {IamService} from '$lib/server/api/services/iam.service'; import { IamService } from '$lib/server/api/iam/iam.service';
import {SessionsService} from '$lib/server/api/services/sessions.service'; import { SessionsService } from '$lib/server/api/iam/sessions/sessions.service';
import {UsersService} from '$lib/server/api/services/users.service'; import { UsersService } from '$lib/server/api/users/users.service';
import {faker} from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import { Container } from '@needle-di/core'; import { Container } from '@needle-di/core';
import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest'; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
describe('IamService', () => { describe('IamService', () => {
let service: IamService; let service: IamService;

View file

@ -1,46 +1,46 @@
import 'reflect-metadata' import 'reflect-metadata';
import { Container } from '@needle-di/core' import { Container } from '@needle-di/core';
import {afterAll, beforeAll, describe, expect, expectTypeOf, it, vi} from 'vitest' import { afterAll, beforeAll, describe, expect, expectTypeOf, it, vi } from 'vitest';
import {HashingService} from '../services/hashing.service' import { HashingService } from '../common/services/hashing.service';
import {TokensService} from '../services/tokens.service' import { TokensService } from '../services/tokens.service';
describe('TokensService', () => { describe('TokensService', () => {
const container = new Container() const container = new Container();
let service: TokensService let service: TokensService;
const hashingService = vi.mocked(HashingService.prototype) const hashingService = vi.mocked(HashingService.prototype);
beforeAll(() => { beforeAll(() => {
container.bind<HashingService>({ provide: HashingService, useValue: hashingService }); container.bind<HashingService>({ provide: HashingService, useValue: hashingService });
service = container.get(TokensService); service = container.get(TokensService);
}) });
afterAll(() => { afterAll(() => {
vi.resetAllMocks() vi.resetAllMocks();
}) });
describe('Generate Token', () => { describe('Generate Token', () => {
it('should resolve', async () => { it('should resolve', async () => {
const hashedPassword = 'testhash' const hashedPassword = 'testhash';
hashingService.hash = vi.fn().mockResolvedValue(hashedPassword) hashingService.hash = vi.fn().mockResolvedValue(hashedPassword);
const spy_hashingService_hash = vi.spyOn(hashingService, 'hash') const spy_hashingService_hash = vi.spyOn(hashingService, 'hash');
const spy_hashingService_verify = vi.spyOn(hashingService, 'verify') const spy_hashingService_verify = vi.spyOn(hashingService, 'verify');
await expectTypeOf(service.createHashedToken('111')).resolves.toBeString() await expectTypeOf(service.createHashedToken('111')).resolves.toBeString();
expect(spy_hashingService_hash).toBeCalledTimes(1) expect(spy_hashingService_hash).toBeCalledTimes(1);
expect(spy_hashingService_verify).toBeCalledTimes(0) expect(spy_hashingService_verify).toBeCalledTimes(0);
}) });
it('should generate a token that is verifiable', async () => { it('should generate a token that is verifiable', async () => {
hashingService.hash = vi.fn().mockResolvedValue('testhash') hashingService.hash = vi.fn().mockResolvedValue('testhash');
hashingService.verify = vi.fn().mockResolvedValue(true) hashingService.verify = vi.fn().mockResolvedValue(true);
const spy_hashingService_hash = vi.spyOn(hashingService, 'hash') const spy_hashingService_hash = vi.spyOn(hashingService, 'hash');
const spy_hashingService_verify = vi.spyOn(hashingService, 'verify') const spy_hashingService_verify = vi.spyOn(hashingService, 'verify');
const token = await service.createHashedToken('111') const token = await service.createHashedToken('111');
expect(token).not.toBeNaN() expect(token).not.toBeNaN();
expect(token).not.toBeUndefined() expect(token).not.toBeUndefined();
expect(token).not.toBeNull() expect(token).not.toBeNull();
const verifiable = await service.verifyHashedToken(token, '111') const verifiable = await service.verifyHashedToken(token, '111');
expect(verifiable).toBeTruthy() expect(verifiable).toBeTruthy();
expect(spy_hashingService_hash).toBeCalledTimes(1) expect(spy_hashingService_hash).toBeCalledTimes(1);
expect(spy_hashingService_verify).toBeCalledTimes(1) expect(spy_hashingService_verify).toBeCalledTimes(1);
}) });
}) });
}) });

View file

@ -1,11 +1,11 @@
import 'reflect-metadata'; import 'reflect-metadata';
import {faker} from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import { Container } from '@needle-di/core'; import { Container } from '@needle-di/core';
import {afterAll, beforeAll, describe, expect, it, vi} from 'vitest'; import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import {RoleName} from '../databases/postgres/tables'; import { RoleName } from '../databases/postgres/tables';
import {UserRolesRepository} from '../repositories/user_roles.repository'; import { RolesService } from '../users/roles.service';
import {RolesService} from '../services/roles.service'; import { UserRolesRepository } from '../users/user_roles.repository';
import {UserRolesService} from '../services/user_roles.service'; import { UserRolesService } from '../users/user_roles.service';
describe('UserRolesService', () => { describe('UserRolesService', () => {
const container = new Container(); const container = new Container();

View file

@ -1,16 +1,16 @@
import 'reflect-metadata'; import 'reflect-metadata';
import {faker} from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import { Container } from '@needle-di/core'; import { Container } from '@needle-di/core';
import {afterAll, beforeAll, describe, expect, it, vi} from 'vitest'; import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import {CredentialsType} from '../databases/postgres/tables'; import { CollectionsService } from '../collections/collections.service';
import {CredentialsRepository} from '../repositories/credentials.repository'; import { DrizzleService } from '../databases/postgres/drizzle.service';
import {UsersRepository} from '../repositories/users.repository'; import { CredentialsType } from '../databases/postgres/tables';
import {CollectionsService} from '../services/collections.service'; import { TokensService } from '../services/tokens.service';
import {DrizzleService} from '../services/drizzle.service'; import { CredentialsRepository } from '../users/credentials.repository';
import {TokensService} from '../services/tokens.service'; import { UserRolesService } from '../users/user_roles.service';
import {UserRolesService} from '../services/user_roles.service'; import { UsersRepository } from '../users/users.repository';
import {UsersService} from '../services/users.service'; import { UsersService } from '../users/users.service';
import {WishlistsService} from '../services/wishlists.service'; import { WishlistsService } from '../wishlists/wishlists.service';
describe('UsersService', () => { describe('UsersService', () => {
const container = new Container(); const container = new Container();
@ -91,7 +91,7 @@ describe('UsersService', () => {
tokensService.createHashedToken = vi.fn().mockResolvedValue(hashedPassword); tokensService.createHashedToken = vi.fn().mockResolvedValue(hashedPassword);
drizzleService.db.transaction = vi.fn().mockImplementation(async (callback) => { drizzleService.db.transaction = vi.fn().mockImplementation(async (callback) => {
return dbUser satisfies Awaited<ReturnType<typeof callback>> return dbUser satisfies Awaited<ReturnType<typeof callback>>;
}); });
const spy_tokensService_createHashToken = vi.spyOn(tokensService, 'createHashedToken'); const spy_tokensService_createHashToken = vi.spyOn(tokensService, 'createHashedToken');

View file

@ -0,0 +1,70 @@
import { DrizzleService } from '$lib/server/api/databases/postgres/drizzle.service';
import { CredentialsType, credentialsTable } from '$lib/server/api/databases/postgres/tables/credentials.table';
import { inject, injectable } from '@needle-di/core';
import { type InferInsertModel, and, eq } from 'drizzle-orm';
import { takeFirstOrThrow } from '../common/utils/repository';
export type CreateCredentials = InferInsertModel<typeof credentialsTable>;
export type UpdateCredentials = Partial<CreateCredentials>;
export type DeleteCredentials = Pick<CreateCredentials, 'id'>;
@injectable()
export class CredentialsRepository {
constructor(private drizzle = inject(DrizzleService)) {}
async findOneByUserId(userId: string, db = this.drizzle.db) {
return db.query.credentialsTable.findFirst({
where: eq(credentialsTable.user_id, userId),
});
}
async findOneByUserIdAndType(userId: string, type: CredentialsType, db = this.drizzle.db) {
return db.query.credentialsTable.findFirst({
where: and(eq(credentialsTable.user_id, userId), eq(credentialsTable.type, type)),
});
}
async findPasswordCredentialsByUserId(userId: string, db = this.drizzle.db) {
return db.query.credentialsTable.findFirst({
where: and(eq(credentialsTable.user_id, userId), eq(credentialsTable.type, CredentialsType.PASSWORD)),
});
}
async findTOTPCredentialsByUserId(userId: string, db = this.drizzle.db) {
return db.query.credentialsTable.findFirst({
where: and(eq(credentialsTable.user_id, userId), eq(credentialsTable.type, CredentialsType.TOTP)),
});
}
async findOneById(id: string, db = this.drizzle.db) {
return db.query.credentialsTable.findFirst({
where: eq(credentialsTable.id, id),
});
}
async findOneByIdOrThrow(id: string, db = this.drizzle.db) {
const credentials = await this.findOneById(id, db);
if (!credentials) throw Error('Credentials not found');
return credentials;
}
async create(data: CreateCredentials, db = this.drizzle.db) {
return db.insert(credentialsTable).values(data).returning().then(takeFirstOrThrow);
}
async update(id: string, data: UpdateCredentials, db = this.drizzle.db) {
return db.update(credentialsTable).set(data).where(eq(credentialsTable.id, id)).returning().then(takeFirstOrThrow);
}
async delete(id: string, db = this.drizzle.db) {
return db.delete(credentialsTable).where(eq(credentialsTable.id, id));
}
async deleteByUserId(userId: string, db = this.drizzle.db) {
return db.delete(credentialsTable).where(eq(credentialsTable.user_id, userId));
}
async deleteByUserIdAndType(userId: string, type: CredentialsType, db = this.drizzle.db) {
return db.delete(credentialsTable).where(and(eq(credentialsTable.user_id, userId), eq(credentialsTable.type, type)));
}
}

View file

@ -0,0 +1,14 @@
import type { z } from 'zod';
import { userDto } from './user.dto';
export const updateUserDto = userDto
.pick({
firstName: true,
lastName: true,
email: true,
username: true,
avatar: true,
})
.optional();
export type UpdateUserDto = z.infer<typeof updateUserDto>;

View file

@ -0,0 +1,23 @@
import { z } from 'zod';
const MAX_UPLOAD_SIZE = 1024 * 1024 * 3; // 3MB
const ACCEPTED_FILE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'];
export const userDto = z.object({
id: z.string(),
firstName: z.string().trim().optional(),
lastName: z.string().trim().optional(),
email: z.string().trim().max(64, { message: 'Email must be less than 64 characters' }).optional(),
username: z.string().trim().min(3, { message: 'Must be at least 3 characters' }).max(50, { message: 'Must be less than 50 characters' }),
avatar: z
.instanceof(File)
.optional()
.refine((file) => {
return !file || file.size <= MAX_UPLOAD_SIZE;
}, 'File size must be less than 3MB')
.refine((file) => {
return ACCEPTED_FILE_TYPES.includes(file!.type);
}, 'File must be a PNG'),
});
export type UserDto = z.infer<typeof userDto>;

View file

@ -0,0 +1,28 @@
import { inject, injectable } from '@needle-di/core';
import { type InferInsertModel, and, eq } from 'drizzle-orm';
import { takeFirstOrThrow } from '../common/utils/repository';
import { DrizzleService } from '../databases/postgres/drizzle.service';
import { federatedIdentityTable } from '../databases/postgres/tables';
export type CreateFederatedIdentity = InferInsertModel<typeof federatedIdentityTable>;
@injectable()
export class FederatedIdentityRepository {
constructor(private drizzle = inject(DrizzleService)) {}
async findOneByUserIdAndProvider(userId: string, provider: string) {
return this.drizzle.db.query.federatedIdentityTable.findFirst({
where: and(eq(federatedIdentityTable.user_id, userId), eq(federatedIdentityTable.identity_provider, provider)),
});
}
async findOneByFederatedUserIdAndProvider(federatedUserId: string, provider: string) {
return this.drizzle.db.query.federatedIdentityTable.findFirst({
where: and(eq(federatedIdentityTable.federated_user_id, federatedUserId), eq(federatedIdentityTable.identity_provider, provider)),
});
}
async create(data: CreateFederatedIdentity, db = this.drizzle.db) {
return db.insert(federatedIdentityTable).values(data).returning().then(takeFirstOrThrow);
}
}

View file

@ -0,0 +1,32 @@
import { takeFirstOrThrow } from '$lib/server/api/common/utils/repository';
import { DrizzleService } from '$lib/server/api/databases/postgres/drizzle.service';
import { inject, injectable } from '@needle-di/core';
import { type InferInsertModel, and, eq } from 'drizzle-orm';
import { recoveryCodesTable } from '../databases/postgres/tables';
export type CreateRecoveryCodes = InferInsertModel<typeof recoveryCodesTable>;
@injectable()
export class RecoveryCodesRepository {
constructor(private drizzle = inject(DrizzleService)) {}
async create(data: CreateRecoveryCodes, db = this.drizzle.db) {
return db.insert(recoveryCodesTable).values(data).returning().then(takeFirstOrThrow);
}
async findAllByUserId(userId: string, db = this.drizzle.db) {
return db.query.recoveryCodesTable.findMany({
where: eq(recoveryCodesTable.userId, userId),
});
}
async findAllNotUsedByUserId(userId: string, db = this.drizzle.db) {
return db.query.recoveryCodesTable.findMany({
where: and(eq(recoveryCodesTable.userId, userId), eq(recoveryCodesTable.used, false)),
});
}
async deleteAllByUserId(userId: string, db = this.drizzle.db) {
return db.delete(recoveryCodesTable).where(eq(recoveryCodesTable.userId, userId));
}
}

View file

@ -0,0 +1,40 @@
import { RecoveryCodesRepository } from '$lib/server/api/users/recovery-codes.repository';
import { inject, injectable } from '@needle-di/core';
import { alphabet, generateRandomString } from 'oslo/crypto';
import { HashingService } from '../common/services/hashing.service';
@injectable()
export class RecoveryCodesService {
constructor(
private hashingService = inject(HashingService),
private recoveryCodesRepository = inject(RecoveryCodesRepository),
) {}
async findAllRecoveryCodesByUserId(userId: string) {
return this.recoveryCodesRepository.findAllByUserId(userId);
}
async createRecoveryCodes(userId: string) {
const createdRecoveryCodes = Array.from({ length: 5 }, () => generateRandomString(10, alphabet('A-Z', '0-9')));
if (createdRecoveryCodes && userId) {
for (const code of createdRecoveryCodes) {
const hashedCode = await this.hashingService.hash(code);
console.log('Inserting recovery code', code, hashedCode);
await this.recoveryCodesRepository.create({ userId, code: hashedCode });
}
return createdRecoveryCodes;
}
return [];
}
async verify(userId: string, code: string) {
const recoveryCodes = await this.recoveryCodesRepository.findAllNotUsedByUserId(userId);
return recoveryCodes.find((recoveryCode) => this.hashingService.verify(recoveryCode.code, code));
}
async deleteAllRecoveryCodesByUserId(userId: string) {
return this.recoveryCodesRepository.deleteAllByUserId(userId);
}
}

View file

@ -0,0 +1,53 @@
import { DrizzleService } from '$lib/server/api/databases/postgres/drizzle.service';
import { inject, injectable } from '@needle-di/core';
import { type InferInsertModel, eq } from 'drizzle-orm';
import { takeFirstOrThrow } from '../common/utils/repository';
import { rolesTable } from '../databases/postgres/tables';
export type CreateRole = InferInsertModel<typeof rolesTable>;
export type UpdateRole = Partial<CreateRole>;
@injectable()
export class RolesRepository {
constructor(private drizzle = inject(DrizzleService)) {}
async findOneById(id: string, db = this.drizzle.db) {
return db.query.rolesTable.findFirst({
where: eq(rolesTable.id, id),
});
}
async findOneByIdOrThrow(id: string, db = this.drizzle.db) {
const role = await this.findOneById(id, db);
if (!role) throw Error('Role not found');
return role;
}
async findAll(db = this.drizzle.db) {
return db.query.rolesTable.findMany();
}
async findOneByName(name: string, db = this.drizzle.db) {
return db.query.rolesTable.findFirst({
where: eq(rolesTable.name, name),
});
}
async findOneByNameOrThrow(name: string, db = this.drizzle.db) {
const role = await this.findOneByName(name, db);
if (!role) throw Error('Role not found');
return role;
}
async create(data: CreateRole, db = this.drizzle.db) {
return db.insert(rolesTable).values(data).returning().then(takeFirstOrThrow);
}
async update(id: string, data: UpdateRole, db = this.drizzle.db) {
return db.update(rolesTable).set(data).where(eq(rolesTable.id, id)).returning().then(takeFirstOrThrow);
}
async delete(id: string, db = this.drizzle.db) {
return db.delete(rolesTable).where(eq(rolesTable.id, id)).returning().then(takeFirstOrThrow);
}
}

Some files were not shown because too many files have changed in this diff Show more