mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
Refactor to how v2 Tofustack works.
This commit is contained in:
parent
85050d0ec4
commit
20e37c4f18
117 changed files with 3611 additions and 3276 deletions
|
|
@ -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'
|
||||||
|
|
|
||||||
281
package.json
281
package.json
|
|
@ -1,143 +1,142 @@
|
||||||
{
|
{
|
||||||
"name": "boredgame",
|
"name": "boredgame",
|
||||||
"version": "0.0.5",
|
"version": "0.0.5",
|
||||||
"private": "true",
|
"private": "true",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"db:push": "drizzle-kit push",
|
"db:push": "drizzle-kit push",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "tsx src/lib/server/api/databases/postgres/migrate.ts",
|
"db:migrate": "tsx src/lib/server/api/databases/postgres/migrate.ts",
|
||||||
"db:seed": "tsx src/lib/server/api/databases/postgres/seed.ts",
|
"db:seed": "tsx src/lib/server/api/databases/postgres/seed.ts",
|
||||||
"db:studio": "drizzle-kit studio --verbose",
|
"db:studio": "drizzle-kit studio --verbose",
|
||||||
"dev": "NODE_OPTIONS=\"--inspect\" vite dev --host",
|
"dev": "NODE_OPTIONS=\"--inspect\" vite dev --host",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"package": "svelte-kit package",
|
"package": "svelte-kit package",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
"test:ui": "svelte-kit sync && playwright test --ui",
|
"test:ui": "svelte-kit sync && playwright test --ui",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"initialize": "pnpm install && docker compose up --no-recreate -d && pnpm db:migrate && pnpm db:seed",
|
"initialize": "pnpm install && docker compose up --no-recreate -d && pnpm db:migrate && pnpm db:seed",
|
||||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||||
"format": "prettier --plugin-search-dir . --write .",
|
"format": "prettier --plugin-search-dir . --write .",
|
||||||
"site:update": "pnpm update -i -L",
|
"site:update": "pnpm update -i -L",
|
||||||
"test:unit": "vitest"
|
"test:unit": "vitest"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^1.9.4",
|
"@biomejs/biome": "^1.9.4",
|
||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^8.4.1",
|
||||||
"@melt-ui/pp": "^0.3.2",
|
"@melt-ui/pp": "^0.3.2",
|
||||||
"@melt-ui/svelte": "^0.83.0",
|
"@melt-ui/svelte": "^0.83.0",
|
||||||
"@playwright/test": "^1.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",
|
"postcss": "^8.4.49",
|
||||||
"postcss": "^8.4.49",
|
"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.4.1",
|
||||||
"prettier": "^3.3.3",
|
"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",
|
"svelte-headless-table": "^0.18.3",
|
||||||
"svelte-headless-table": "^0.18.3",
|
"svelte-meta-tags": "^3.1.4",
|
||||||
"svelte-meta-tags": "^3.1.4",
|
"svelte-preprocess": "^6.0.3",
|
||||||
"svelte-preprocess": "^6.0.3",
|
"svelte-sequential-preprocessor": "^2.0.2",
|
||||||
"svelte-sequential-preprocessor": "^2.0.2",
|
"svelte-sonner": "^0.3.28",
|
||||||
"svelte-sonner": "^0.3.28",
|
"sveltekit-flash-message": "^2.4.4",
|
||||||
"sveltekit-flash-message": "^2.4.4",
|
"sveltekit-superforms": "^2.20.1",
|
||||||
"sveltekit-superforms": "^2.20.1",
|
"tailwindcss": "^3.4.15",
|
||||||
"tailwindcss": "^3.4.15",
|
"ts-node": "^10.9.2",
|
||||||
"ts-node": "^10.9.2",
|
"tslib": "^2.8.1",
|
||||||
"tslib": "^2.8.1",
|
"tsx": "^4.19.2",
|
||||||
"tsx": "^4.19.2",
|
"typescript": "^5.7.2",
|
||||||
"typescript": "^5.7.2",
|
"vite": "^5.4.11",
|
||||||
"vite": "^5.4.11",
|
"vitest": "^1.6.0",
|
||||||
"vitest": "^1.6.0",
|
"zod": "^3.23.8"
|
||||||
"zod": "^3.23.8"
|
},
|
||||||
},
|
"type": "module",
|
||||||
"type": "module",
|
"dependencies": {
|
||||||
"dependencies": {
|
"@fontsource/fira-mono": "^5.1.0",
|
||||||
"@fontsource/fira-mono": "^5.1.0",
|
"@hono/swagger-ui": "^0.4.1",
|
||||||
"@hono/swagger-ui": "^0.4.1",
|
"@hono/zod-openapi": "^0.15.3",
|
||||||
"@hono/zod-openapi": "^0.15.3",
|
"@hono/zod-validator": "^0.2.2",
|
||||||
"@hono/zod-validator": "^0.2.2",
|
"@iconify-icons/line-md": "^1.2.30",
|
||||||
"@iconify-icons/line-md": "^1.2.30",
|
"@iconify-icons/mdi": "^1.2.48",
|
||||||
"@iconify-icons/mdi": "^1.2.48",
|
"@inlang/paraglide-sveltekit": "^0.11.1",
|
||||||
"@inlang/paraglide-sveltekit": "^0.11.1",
|
"@internationalized/date": "^3.6.0",
|
||||||
"@internationalized/date": "^3.6.0",
|
"@lucia-auth/adapter-drizzle": "^1.1.0",
|
||||||
"@lucia-auth/adapter-drizzle": "^1.1.0",
|
"@lukeed/uuid": "^2.0.1",
|
||||||
"@lukeed/uuid": "^2.0.1",
|
"@needle-di/core": "^0.8.4",
|
||||||
"@needle-di/core": "^0.8.4",
|
"@neondatabase/serverless": "^0.9.5",
|
||||||
"@neondatabase/serverless": "^0.9.5",
|
"@node-rs/argon2": "^1.8.3",
|
||||||
"@node-rs/argon2": "^1.8.3",
|
"@oslojs/binary": "^1.0.0",
|
||||||
"@oslojs/binary": "^1.0.0",
|
"@oslojs/crypto": "^1.0.1",
|
||||||
"@oslojs/crypto": "^1.0.1",
|
"@oslojs/encoding": "^1.1.0",
|
||||||
"@oslojs/encoding": "^1.1.0",
|
"@oslojs/jwt": "^0.2.0",
|
||||||
"@oslojs/jwt": "^0.2.0",
|
"@oslojs/oauth2": "^0.5.0",
|
||||||
"@oslojs/oauth2": "^0.5.0",
|
"@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.162",
|
||||||
"@scalar/hono-api-reference": "^0.5.161",
|
"@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",
|
||||||
"dotenv": "^16.4.5",
|
"dayjs": "^1.11.13",
|
||||||
"dotenv-expand": "^11.0.7",
|
"dotenv": "^16.4.5",
|
||||||
"drizzle-orm": "^0.36.4",
|
"dotenv-expand": "^11.0.7",
|
||||||
"drizzle-zod": "^0.5.1",
|
"drizzle-orm": "^0.36.4",
|
||||||
"feather-icons": "^4.29.2",
|
"drizzle-zod": "^0.5.1",
|
||||||
"handlebars": "^4.7.8",
|
"feather-icons": "^4.29.2",
|
||||||
"hono": "^4.6.11",
|
"handlebars": "^4.7.8",
|
||||||
"hono-pino": "^0.7.0",
|
"hono": "^4.6.12",
|
||||||
"hono-rate-limiter": "^0.4.0",
|
"hono-pino": "^0.7.0",
|
||||||
"hono-zod-openapi": "^0.5.0",
|
"hono-rate-limiter": "^0.4.0",
|
||||||
"html-entities": "^2.5.2",
|
"hono-zod-openapi": "^0.5.0",
|
||||||
"iconify-icon": "^2.1.0",
|
"html-entities": "^2.5.2",
|
||||||
"ioredis": "^5.4.1",
|
"iconify-icon": "^2.1.0",
|
||||||
"just-capitalize": "^3.2.0",
|
"ioredis": "^5.4.1",
|
||||||
"just-kebab-case": "^4.2.0",
|
"just-capitalize": "^3.2.0",
|
||||||
"loader": "^2.1.1",
|
"just-kebab-case": "^4.2.0",
|
||||||
"nanoid": "^5.0.8",
|
"loader": "^2.1.1",
|
||||||
"open-props": "^1.7.7",
|
"nanoid": "^5.0.9",
|
||||||
"oslo": "^1.2.1",
|
"open-props": "^1.7.7",
|
||||||
"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",
|
||||||
"postgres": "^3.4.5",
|
"postgres": "^3.4.5",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"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"
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
381
pnpm-lock.yaml
381
pnpm-lock.yaml
|
|
@ -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
2
src/app.d.ts
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,45 @@
|
||||||
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') || '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ----------------------------- Auth functions ----------------------------- */
|
/* ----------------------------- Auth functions ----------------------------- */
|
||||||
async function getAuthedUser() {
|
async function getAuthedUser() {
|
||||||
const { data } = await api.me.$get().then(parseApiResponse);
|
const { data } = await api.me.$get().then(parseApiResponse);
|
||||||
return { user: data?.user, session: data?.session };
|
return { user: data?.user, session: data?.session };
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------ Set contexts ------------------------------ */
|
/* ------------------------------ Set contexts ------------------------------ */
|
||||||
event.locals.api = api;
|
event.locals.api = api;
|
||||||
event.locals.parseApiResponse = parseApiResponse;
|
event.locals.parseApiResponse = parseApiResponse;
|
||||||
event.locals.getAuthedUser = getAuthedUser;
|
event.locals.getAuthedUser = getAuthedUser;
|
||||||
event.locals.getAuthedUserOrThrow = getAuthedUserOrThrow;
|
event.locals.getAuthedUserOrThrow = getAuthedUserOrThrow;
|
||||||
|
|
||||||
/* ----------------------------- Return response ---------------------------- */
|
/* ----------------------------- Return response ---------------------------- */
|
||||||
return await resolve(event);
|
return await resolve(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handle: Handle = sequence(apiClient, handleParaglide);
|
export const handle: Handle = sequence(apiClient, handleParaglide);
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
74
src/lib/server/api/application.controller.ts
Normal file
74
src/lib/server/api/application.controller.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/lib/server/api/application.module.ts
Normal file
42
src/lib/server/api/application.module.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
68
src/lib/server/api/collections/collection.routes.ts
Normal file
68
src/lib/server/api/collections/collection.routes.ts
Normal 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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
83
src/lib/server/api/collections/collections.repository.ts
Normal file
83
src/lib/server/api/collections/collections.repository.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/lib/server/api/collections/collections.service.ts
Normal file
50
src/lib/server/api/collections/collections.service.ts
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/lib/server/api/common/configs/config.service.ts
Normal file
32
src/lib/server/api/common/configs/config.service.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/lib/server/api/common/configs/dtos/env.dto.ts
Normal file
39
src/lib/server/api/common/configs/dtos/env.dto.ts
Normal 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>;
|
||||||
|
|
@ -1,39 +1,39 @@
|
||||||
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
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
13
src/lib/server/api/common/factories/controllers.factory.ts
Normal file
13
src/lib/server/api/common/factories/controllers.factory.ts
Normal 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>;
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 = '';
|
||||||
|
}
|
||||||
47
src/lib/server/api/common/middleware/auth.middleware.ts
Normal file
47
src/lib/server/api/common/middleware/auth.middleware.ts
Normal 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();
|
||||||
|
});
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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';
|
||||||
|
|
@ -17,14 +17,14 @@ export const requireFullAuth: MiddlewareHandler<{
|
||||||
});
|
});
|
||||||
|
|
||||||
export const requireTempAuth: MiddlewareHandler<{
|
export const requireTempAuth: MiddlewareHandler<{
|
||||||
Variables: {
|
Variables: {
|
||||||
session: Sessions;
|
session: Sessions;
|
||||||
user: Users;
|
user: Users;
|
||||||
};
|
};
|
||||||
}> = createMiddleware(async (c, next) => {
|
}> = createMiddleware(async (c, next) => {
|
||||||
const session = c.var.session;
|
const session = c.var.session;
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw Unauthorized('You must be logged in to access this resource');
|
throw Unauthorized('You must be logged in to access this resource');
|
||||||
}
|
}
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
|
@ -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 {
|
||||||
38
src/lib/server/api/common/services/hashing.service.test.ts
Normal file
38
src/lib/server/api/common/services/hashing.service.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
13
src/lib/server/api/common/services/hashing.service.ts
Normal file
13
src/lib/server/api/common/services/hashing.service.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,34 +1,34 @@
|
||||||
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>;
|
||||||
|
|
||||||
export type AppBindings = {
|
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>;
|
||||||
resetKey: (key: string) => Promisify<void>;
|
resetKey: (key: string) => Promisify<void>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HonoTypes = {
|
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>;
|
||||||
resetKey: (key: string) => Promisify<void>;
|
resetKey: (key: string) => Promisify<void>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,63 +1,62 @@
|
||||||
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);
|
||||||
export const cookieName = 'session';
|
export const cookieName = 'session';
|
||||||
|
|
||||||
export type SessionCookie = {
|
export type SessionCookie = {
|
||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
attributes: CookieOptions;
|
attributes: CookieOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createSessionTokenCookie(token: string, expiresAt: Date): SessionCookie {
|
export function createSessionTokenCookie(token: string, expiresAt: Date): SessionCookie {
|
||||||
return {
|
return {
|
||||||
name: cookieName,
|
name: cookieName,
|
||||||
value: token,
|
value: token,
|
||||||
attributes: {
|
attributes: {
|
||||||
path: '/',
|
path: '/',
|
||||||
maxAge: cookieMaxAge,
|
maxAge: cookieMaxAge,
|
||||||
domain: env.DOMAIN,
|
domain: env.DOMAIN,
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
secure: config.isProduction,
|
secure: config.isProduction,
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
expires: expiresAt,
|
expires: expiresAt,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createBlankSessionTokenCookie(): SessionCookie {
|
export function createBlankSessionTokenCookie(): SessionCookie {
|
||||||
return {
|
return {
|
||||||
name: cookieName,
|
name: cookieName,
|
||||||
value: '',
|
value: '',
|
||||||
attributes: {
|
attributes: {
|
||||||
path: '/',
|
path: '/',
|
||||||
maxAge: 0,
|
maxAge: 0,
|
||||||
domain: env.DOMAIN,
|
domain: env.DOMAIN,
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
secure: config.isProduction,
|
secure: config.isProduction,
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
expires: new Date(0),
|
expires: new Date(0),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setSessionCookie(c: Context, sessionCookie: SessionCookie) {
|
export function setSessionCookie(c: Context, sessionCookie: SessionCookie) {
|
||||||
setCookie(c, sessionCookie.name, sessionCookie.value, {
|
setCookie(c, sessionCookie.name, sessionCookie.value, {
|
||||||
path: sessionCookie.attributes.path,
|
path: sessionCookie.attributes.path,
|
||||||
maxAge: sessionCookie.attributes?.maxAge,
|
maxAge: sessionCookie.attributes?.maxAge,
|
||||||
domain: sessionCookie.attributes.domain,
|
domain: sessionCookie.attributes.domain,
|
||||||
sameSite: sessionCookie.attributes.sameSite as undefined,
|
sameSite: sessionCookie.attributes.sameSite as undefined,
|
||||||
secure: sessionCookie.attributes.secure,
|
secure: sessionCookie.attributes.secure,
|
||||||
httpOnly: sessionCookie.attributes.httpOnly,
|
httpOnly: sessionCookie.attributes.httpOnly,
|
||||||
expires: sessionCookie.attributes.expires,
|
expires: sessionCookie.attributes.expires,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
53
src/lib/server/api/common/utils/drizzle.ts
Normal file
53
src/lib/server/api/common/utils/drizzle.ts
Normal 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()),
|
||||||
|
};
|
||||||
|
|
@ -1,40 +1,40 @@
|
||||||
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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tooManyRequestsSchema = createMessageObjectSchema(HttpStatusPhrases.TOO_MANY_REQUESTS);
|
export const tooManyRequestsSchema = createMessageObjectSchema(HttpStatusPhrases.TOO_MANY_REQUESTS);
|
||||||
|
|
||||||
export function Forbidden(message = 'Forbidden') {
|
export function Forbidden(message = 'Forbidden') {
|
||||||
return new HTTPException(StatusCodes.FORBIDDEN, { message });
|
return new HTTPException(StatusCodes.FORBIDDEN, { message });
|
||||||
}
|
}
|
||||||
|
|
||||||
export const forbiddenSchema = createMessageObjectSchema(HttpStatusPhrases.FORBIDDEN);
|
export const forbiddenSchema = createMessageObjectSchema(HttpStatusPhrases.FORBIDDEN);
|
||||||
|
|
||||||
export function Unauthorized(message = 'Unauthorized') {
|
export function Unauthorized(message = 'Unauthorized') {
|
||||||
return new HTTPException(StatusCodes.UNAUTHORIZED, { message });
|
return new HTTPException(StatusCodes.UNAUTHORIZED, { message });
|
||||||
}
|
}
|
||||||
|
|
||||||
export const unauthorizedSchema = createMessageObjectSchema(HttpStatusPhrases.UNAUTHORIZED);
|
export const unauthorizedSchema = createMessageObjectSchema(HttpStatusPhrases.UNAUTHORIZED);
|
||||||
|
|
||||||
export function NotFound(message = 'Not Found') {
|
export function NotFound(message = 'Not Found') {
|
||||||
return new HTTPException(StatusCodes.NOT_FOUND, { message });
|
return new HTTPException(StatusCodes.NOT_FOUND, { message });
|
||||||
}
|
}
|
||||||
|
|
||||||
export const notFoundSchema = createMessageObjectSchema(HttpStatusPhrases.NOT_FOUND);
|
export const notFoundSchema = createMessageObjectSchema(HttpStatusPhrases.NOT_FOUND);
|
||||||
|
|
||||||
export function BadRequest(message = 'Bad Request') {
|
export function BadRequest(message = 'Bad Request') {
|
||||||
return new HTTPException(StatusCodes.BAD_REQUEST, { message });
|
return new HTTPException(StatusCodes.BAD_REQUEST, { message });
|
||||||
}
|
}
|
||||||
|
|
||||||
export const badRequestSchema = createMessageObjectSchema(HttpStatusPhrases.BAD_REQUEST);
|
export const badRequestSchema = createMessageObjectSchema(HttpStatusPhrases.BAD_REQUEST);
|
||||||
|
|
||||||
export function InternalError(message = 'Internal Error') {
|
export function InternalError(message = 'Internal Error') {
|
||||||
return new HTTPException(StatusCodes.INTERNAL_SERVER_ERROR, { message });
|
return new HTTPException(StatusCodes.INTERNAL_SERVER_ERROR, { message });
|
||||||
}
|
}
|
||||||
|
|
||||||
export const internalErrorSchema = createMessageObjectSchema(HttpStatusPhrases.INTERNAL_SERVER_ERROR);
|
export const internalErrorSchema = createMessageObjectSchema(HttpStatusPhrases.INTERNAL_SERVER_ERROR);
|
||||||
20
src/lib/server/api/common/utils/hono.ts
Normal file
20
src/lib/server/api/common/utils/hono.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,70 +1,42 @@
|
||||||
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, {
|
||||||
info: {
|
info: {
|
||||||
title: 'Bored Game API',
|
title: 'Bored Game API',
|
||||||
description: 'Bored Game API',
|
description: 'Bored Game API',
|
||||||
version: packageJSON.version,
|
version: packageJSON.version,
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
securitySchemes: {
|
securitySchemes: {
|
||||||
bearerAuth: {
|
bearerAuth: {
|
||||||
type: 'http',
|
type: 'http',
|
||||||
scheme: 'bearer',
|
scheme: 'bearer',
|
||||||
},
|
},
|
||||||
cookieAuth: {
|
cookieAuth: {
|
||||||
type: 'apiKey',
|
type: 'apiKey',
|
||||||
name: 'session',
|
name: 'session',
|
||||||
in: 'cookie',
|
in: 'cookie',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get(
|
app.get(
|
||||||
'/reference',
|
'/reference',
|
||||||
apiReference({
|
apiReference({
|
||||||
theme: 'kepler',
|
theme: 'kepler',
|
||||||
layout: 'classic',
|
layout: 'classic',
|
||||||
defaultHttpClient: {
|
defaultHttpClient: {
|
||||||
targetKey: 'javascript',
|
targetKey: 'javascript',
|
||||||
clientKey: 'fetch',
|
clientKey: 'fetch',
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
url: '/api/doc',
|
url: '/api/doc',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -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',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -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',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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' });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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 });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
31
src/lib/server/api/databases/postgres/drizzle.service.ts
Normal file
31
src/lib/server/api/databases/postgres/drizzle.service.ts
Normal 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',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
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();
|
||||||
|
|
||||||
if (!config.out) {
|
if (!config.out) {
|
||||||
console.error('No migrations folder specified in drizzle.config.ts');
|
console.error('No migrations folder specified in drizzle.config.ts');
|
||||||
process.exit();
|
process.exit();
|
||||||
}
|
}
|
||||||
if (!env.DB_MIGRATING) {
|
if (!env.DB_MIGRATING) {
|
||||||
throw new Error('You must set DB_MIGRATING to "true" when running migrations.');
|
throw new Error('You must set DB_MIGRATING to "true" when running migrations.');
|
||||||
}
|
}
|
||||||
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();
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,53 @@
|
||||||
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';
|
||||||
|
|
||||||
const drizzleService = new DrizzleService();
|
const drizzleService = new DrizzleService();
|
||||||
|
|
||||||
if (!env.DB_SEEDING) {
|
if (!env.DB_SEEDING) {
|
||||||
throw new Error('You must set DB_SEEDING to "true" when running seeds');
|
throw new Error('You must set DB_SEEDING to "true" when running seeds');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resetTable(db: NodePgDatabase<typeof schema>, table: Table) {
|
async function resetTable(db: NodePgDatabase<typeof schema>, table: Table) {
|
||||||
return db.execute(sql.raw(`TRUNCATE TABLE ${getTableName(table)} RESTART IDENTITY CASCADE`));
|
return db.execute(sql.raw(`TRUNCATE TABLE ${getTableName(table)} RESTART IDENTITY CASCADE`));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const table of [
|
for (const table of [
|
||||||
schema.categoriesTable,
|
schema.categoriesTable,
|
||||||
schema.categoriesToExternalIdsTable,
|
schema.categoriesToExternalIdsTable,
|
||||||
schema.categories_to_games_table,
|
schema.categories_to_games_table,
|
||||||
schema.collection_items,
|
schema.collection_items,
|
||||||
schema.collections,
|
schema.collections,
|
||||||
schema.credentialsTable,
|
schema.credentialsTable,
|
||||||
schema.expansionsTable,
|
schema.expansionsTable,
|
||||||
schema.externalIdsTable,
|
schema.externalIdsTable,
|
||||||
schema.federatedIdentityTable,
|
schema.federatedIdentityTable,
|
||||||
schema.gamesTable,
|
schema.gamesTable,
|
||||||
schema.gamesToExternalIdsTable,
|
schema.gamesToExternalIdsTable,
|
||||||
schema.mechanicsTable,
|
schema.mechanicsTable,
|
||||||
schema.mechanicsToExternalIdsTable,
|
schema.mechanicsToExternalIdsTable,
|
||||||
schema.mechanics_to_games,
|
schema.mechanics_to_games,
|
||||||
schema.password_reset_tokens,
|
schema.password_reset_tokens,
|
||||||
schema.publishersTable,
|
schema.publishersTable,
|
||||||
schema.publishersToExternalIdsTable,
|
schema.publishersToExternalIdsTable,
|
||||||
schema.publishers_to_games,
|
schema.publishers_to_games,
|
||||||
schema.recoveryCodesTable,
|
schema.recoveryCodesTable,
|
||||||
schema.rolesTable,
|
schema.rolesTable,
|
||||||
schema.twoFactorTable,
|
schema.twoFactorTable,
|
||||||
schema.user_roles,
|
schema.user_roles,
|
||||||
schema.usersTable,
|
schema.usersTable,
|
||||||
schema.wishlist_items,
|
schema.wishlist_items,
|
||||||
schema.wishlistsTable,
|
schema.wishlistsTable,
|
||||||
]) {
|
]) {
|
||||||
// await db.delete(table); // clear tables without truncating / resetting ids
|
// await db.delete(table); // clear tables without truncating / resetting ids
|
||||||
await resetTable(drizzleService.db, table);
|
await resetTable(drizzleService.db, table);
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
|
|
|
||||||
|
|
@ -1,89 +1,89 @@
|
||||||
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';
|
||||||
|
|
||||||
type JsonRole = {
|
type JsonRole = {
|
||||||
name: string;
|
name: string;
|
||||||
primary: boolean;
|
primary: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function seed(db: NodePgDatabase<typeof schema>) {
|
export default async function seed(db: NodePgDatabase<typeof schema>) {
|
||||||
const hashingService = new HashingService();
|
const hashingService = new HashingService();
|
||||||
const adminRole = await db.select().from(schema.rolesTable).where(eq(schema.rolesTable.name, 'admin'));
|
const adminRole = await db.select().from(schema.rolesTable).where(eq(schema.rolesTable.name, 'admin'));
|
||||||
const userRole = await db.select().from(schema.rolesTable).where(eq(schema.rolesTable.name, 'user'));
|
const userRole = await db.select().from(schema.rolesTable).where(eq(schema.rolesTable.name, 'user'));
|
||||||
|
|
||||||
const adminUser = await db
|
const adminUser = await db
|
||||||
.insert(schema.usersTable)
|
.insert(schema.usersTable)
|
||||||
.values({
|
.values({
|
||||||
username: `${process.env.ADMIN_USERNAME}`,
|
username: `${process.env.ADMIN_USERNAME}`,
|
||||||
email: '',
|
email: '',
|
||||||
first_name: 'Brad',
|
first_name: 'Brad',
|
||||||
last_name: 'S',
|
last_name: 'S',
|
||||||
verified: true,
|
verified: true,
|
||||||
})
|
})
|
||||||
.returning()
|
.returning()
|
||||||
.onConflictDoNothing();
|
.onConflictDoNothing();
|
||||||
|
|
||||||
await db.insert(schema.credentialsTable).values({
|
await db.insert(schema.credentialsTable).values({
|
||||||
user_id: adminUser[0].id,
|
user_id: adminUser[0].id,
|
||||||
type: schema.CredentialsType.PASSWORD,
|
type: schema.CredentialsType.PASSWORD,
|
||||||
secret_data: await hashingService.hash(`${process.env.ADMIN_PASSWORD}`),
|
secret_data: await hashingService.hash(`${process.env.ADMIN_PASSWORD}`),
|
||||||
});
|
});
|
||||||
|
|
||||||
await db.insert(schema.collections).values({ user_id: adminUser[0].id }).onConflictDoNothing();
|
await db.insert(schema.collections).values({ user_id: adminUser[0].id }).onConflictDoNothing();
|
||||||
|
|
||||||
await db.insert(schema.wishlistsTable).values({ user_id: adminUser[0].id }).onConflictDoNothing();
|
await db.insert(schema.wishlistsTable).values({ user_id: adminUser[0].id }).onConflictDoNothing();
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.insert(schema.user_roles)
|
.insert(schema.user_roles)
|
||||||
.values({
|
.values({
|
||||||
user_id: adminUser[0].id,
|
user_id: adminUser[0].id,
|
||||||
role_id: adminRole[0].id,
|
role_id: adminRole[0].id,
|
||||||
primary: true,
|
primary: true,
|
||||||
})
|
})
|
||||||
.onConflictDoNothing();
|
.onConflictDoNothing();
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.insert(schema.user_roles)
|
.insert(schema.user_roles)
|
||||||
.values({
|
.values({
|
||||||
user_id: adminUser[0].id,
|
user_id: adminUser[0].id,
|
||||||
role_id: userRole[0].id,
|
role_id: userRole[0].id,
|
||||||
primary: false,
|
primary: false,
|
||||||
})
|
})
|
||||||
.onConflictDoNothing();
|
.onConflictDoNothing();
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
users.map(async (user) => {
|
users.map(async (user) => {
|
||||||
const [insertedUser] = await db
|
const [insertedUser] = await db
|
||||||
.insert(schema.usersTable)
|
.insert(schema.usersTable)
|
||||||
.values({
|
.values({
|
||||||
...user,
|
...user,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
await db.insert(schema.credentialsTable).values({
|
await db.insert(schema.credentialsTable).values({
|
||||||
user_id: insertedUser?.id,
|
user_id: insertedUser?.id,
|
||||||
type: schema.CredentialsType.PASSWORD,
|
type: schema.CredentialsType.PASSWORD,
|
||||||
secret_data: await hashingService.hash(user.password),
|
secret_data: await hashingService.hash(user.password),
|
||||||
});
|
});
|
||||||
await db.insert(schema.collections).values({ user_id: insertedUser?.id });
|
await db.insert(schema.collections).values({ user_id: insertedUser?.id });
|
||||||
await db.insert(schema.wishlistsTable).values({ user_id: insertedUser?.id });
|
await db.insert(schema.wishlistsTable).values({ user_id: insertedUser?.id });
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
user.roles.map(async (role: JsonRole) => {
|
user.roles.map(async (role: JsonRole) => {
|
||||||
const foundRole = await db.query.rolesTable.findFirst({
|
const foundRole = await db.query.rolesTable.findFirst({
|
||||||
where: eq(schema.rolesTable.name, role.name),
|
where: eq(schema.rolesTable.name, role.name),
|
||||||
});
|
});
|
||||||
if (!foundRole) {
|
if (!foundRole) {
|
||||||
throw new Error('Role not found');
|
throw new Error('Role not found');
|
||||||
}
|
}
|
||||||
await db.insert(schema.user_roles).values({
|
await db.insert(schema.user_roles).values({
|
||||||
user_id: insertedUser?.id,
|
user_id: insertedUser?.id,
|
||||||
role_id: foundRole?.id,
|
role_id: foundRole?.id,
|
||||||
primary: role?.primary,
|
primary: role?.primary,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,55 @@
|
||||||
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()
|
||||||
.unique()
|
.unique()
|
||||||
.$defaultFn(() => cuid2()),
|
.$defaultFn(() => cuid2()),
|
||||||
username: text().unique(),
|
username: text().unique(),
|
||||||
email: text().unique(),
|
email: text().unique(),
|
||||||
first_name: text(),
|
first_name: text(),
|
||||||
last_name: text(),
|
last_name: text(),
|
||||||
verified: boolean().default(false),
|
verified: boolean().default(false),
|
||||||
receive_email: boolean().default(false),
|
receive_email: boolean().default(false),
|
||||||
email_verified: boolean().default(false),
|
email_verified: boolean().default(false),
|
||||||
picture: text(),
|
picture: text(),
|
||||||
mfa_enabled: boolean().notNull().default(false),
|
mfa_enabled: boolean().notNull().default(false),
|
||||||
theme: text().default('system'),
|
theme: text().default('system'),
|
||||||
...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,
|
||||||
|
};
|
||||||
|
|
|
||||||
35
src/lib/server/api/databases/redis/redis.service.ts
Normal file
35
src/lib/server/api/databases/redis/redis.service.ts
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
145
src/lib/server/api/iam/iam.routes.ts
Normal file
145
src/lib/server/api/iam/iam.routes.ts
Normal 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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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()
|
||||||
14
src/lib/server/api/iam/sessions/dtos/create-session-dto.ts
Normal file
14
src/lib/server/api/iam/sessions/dtos/create-session-dto.ts
Normal 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>;
|
||||||
13
src/lib/server/api/iam/sessions/dtos/session.dto.ts
Normal file
13
src/lib/server/api/iam/sessions/dtos/session.dto.ts
Normal 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>;
|
||||||
31
src/lib/server/api/iam/sessions/sessions.repository.ts
Normal file
31
src/lib/server/api/iam/sessions/sessions.repository.ts
Normal 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),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
92
src/lib/server/api/iam/sessions/sessions.service.ts
Normal file
92
src/lib/server/api/iam/sessions/sessions.service.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
21
src/lib/server/api/login/login.routes.ts
Normal file
21
src/lib/server/api/login/login.routes.ts
Normal 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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -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) {
|
||||||
|
|
@ -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();
|
|
||||||
});
|
|
||||||
|
|
@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
125
src/lib/server/api/oauth/oauth.controller.ts
Normal file
125
src/lib/server/api/oauth/oauth.controller.ts
Normal 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;
|
||||||
|
}
|
||||||
27
src/lib/server/api/oauth/oauth.service.ts
Normal file
27
src/lib/server/api/oauth/oauth.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +1,39 @@
|
||||||
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)
|
||||||
return generateRandomString(6, alphabet);
|
return generateRandomString(6, alphabet);
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
44
src/lib/server/api/signup/signup.controller.ts
Normal file
44
src/lib/server/api/signup/signup.controller.ts
Normal 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' });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,125 +1,125 @@
|
||||||
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;
|
||||||
const container = new Container();
|
const container = new Container();
|
||||||
const sessionService = vi.mocked(SessionsService.prototype);
|
const sessionService = vi.mocked(SessionsService.prototype);
|
||||||
const userService = vi.mocked(UsersService.prototype);
|
const userService = vi.mocked(UsersService.prototype);
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
container
|
container
|
||||||
.bind<SessionsService>({ provide: SessionsService, useValue: sessionService })
|
.bind<SessionsService>({ provide: SessionsService, useValue: sessionService })
|
||||||
.bind<UsersService>({ provide: UsersService, useValue: userService });
|
.bind<UsersService>({ provide: UsersService, useValue: userService });
|
||||||
|
|
||||||
service = container.get(IamService);
|
service = container.get(IamService);
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
vi.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
const timeStampDate = new Date();
|
const timeStampDate = new Date();
|
||||||
const dbUser = {
|
const dbUser = {
|
||||||
id: faker.string.uuid(),
|
id: faker.string.uuid(),
|
||||||
cuid: 'ciglo1j8q0000t9j4xq8d6p5e',
|
cuid: 'ciglo1j8q0000t9j4xq8d6p5e',
|
||||||
first_name: faker.person.firstName(),
|
first_name: faker.person.firstName(),
|
||||||
last_name: faker.person.lastName(),
|
last_name: faker.person.lastName(),
|
||||||
email: faker.internet.email(),
|
email: faker.internet.email(),
|
||||||
username: faker.internet.userName(),
|
username: faker.internet.userName(),
|
||||||
verified: false,
|
verified: false,
|
||||||
receive_email: false,
|
receive_email: false,
|
||||||
mfa_enabled: false,
|
mfa_enabled: false,
|
||||||
theme: 'system',
|
theme: 'system',
|
||||||
createdAt: timeStampDate,
|
createdAt: timeStampDate,
|
||||||
updatedAt: timeStampDate,
|
updatedAt: timeStampDate,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Update Profile', () => {
|
describe('Update Profile', () => {
|
||||||
it('should update user', async () => {
|
it('should update user', async () => {
|
||||||
userService.findOneById = vi.fn().mockResolvedValueOnce(dbUser);
|
userService.findOneById = vi.fn().mockResolvedValueOnce(dbUser);
|
||||||
userService.findOneByUsername = vi.fn().mockResolvedValue(undefined);
|
userService.findOneByUsername = vi.fn().mockResolvedValue(undefined);
|
||||||
userService.updateUser = vi.fn().mockResolvedValue(dbUser);
|
userService.updateUser = vi.fn().mockResolvedValue(dbUser);
|
||||||
|
|
||||||
const spy_userService_findOneById = vi.spyOn(userService, 'findOneById');
|
const spy_userService_findOneById = vi.spyOn(userService, 'findOneById');
|
||||||
const spy_userService_findOneByUsername = vi.spyOn(userService, 'findOneByUsername');
|
const spy_userService_findOneByUsername = vi.spyOn(userService, 'findOneByUsername');
|
||||||
const spy_userService_updateUser = vi.spyOn(userService, 'updateUser');
|
const spy_userService_updateUser = vi.spyOn(userService, 'updateUser');
|
||||||
await expect(
|
await expect(
|
||||||
service.updateProfile(faker.string.uuid(), {
|
service.updateProfile(faker.string.uuid(), {
|
||||||
username: faker.internet.userName(),
|
username: faker.internet.userName(),
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual(dbUser);
|
).resolves.toEqual(dbUser);
|
||||||
expect(spy_userService_findOneById).toBeCalledTimes(1);
|
expect(spy_userService_findOneById).toBeCalledTimes(1);
|
||||||
expect(spy_userService_findOneByUsername).toBeCalledTimes(1);
|
expect(spy_userService_findOneByUsername).toBeCalledTimes(1);
|
||||||
expect(spy_userService_updateUser).toBeCalledTimes(1);
|
expect(spy_userService_updateUser).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should error on no user found', async () => {
|
it('should error on no user found', async () => {
|
||||||
userService.findOneById = vi.fn().mockResolvedValueOnce(undefined);
|
userService.findOneById = vi.fn().mockResolvedValueOnce(undefined);
|
||||||
|
|
||||||
const spy_userService_findOneById = vi.spyOn(userService, 'findOneById');
|
const spy_userService_findOneById = vi.spyOn(userService, 'findOneById');
|
||||||
const spy_userService_findOneByUsername = vi.spyOn(userService, 'findOneByUsername');
|
const spy_userService_findOneByUsername = vi.spyOn(userService, 'findOneByUsername');
|
||||||
const spy_userService_updateUser = vi.spyOn(userService, 'updateUser');
|
const spy_userService_updateUser = vi.spyOn(userService, 'updateUser');
|
||||||
await expect(
|
await expect(
|
||||||
service.updateProfile(faker.string.uuid(), {
|
service.updateProfile(faker.string.uuid(), {
|
||||||
username: faker.internet.userName(),
|
username: faker.internet.userName(),
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual({
|
).resolves.toEqual({
|
||||||
error: 'User not found',
|
error: 'User not found',
|
||||||
});
|
});
|
||||||
expect(spy_userService_findOneById).toBeCalledTimes(1);
|
expect(spy_userService_findOneById).toBeCalledTimes(1);
|
||||||
expect(spy_userService_findOneByUsername).toBeCalledTimes(0);
|
expect(spy_userService_findOneByUsername).toBeCalledTimes(0);
|
||||||
expect(spy_userService_updateUser).toBeCalledTimes(0);
|
expect(spy_userService_updateUser).toBeCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should error on duplicate username', async () => {
|
it('should error on duplicate username', async () => {
|
||||||
userService.findOneById = vi.fn().mockResolvedValueOnce(dbUser);
|
userService.findOneById = vi.fn().mockResolvedValueOnce(dbUser);
|
||||||
userService.findOneByUsername = vi.fn().mockResolvedValue({
|
userService.findOneByUsername = vi.fn().mockResolvedValue({
|
||||||
id: faker.string.uuid(),
|
id: faker.string.uuid(),
|
||||||
});
|
});
|
||||||
userService.updateUser = vi.fn().mockResolvedValue(dbUser);
|
userService.updateUser = vi.fn().mockResolvedValue(dbUser);
|
||||||
|
|
||||||
const spy_userService_findOneById = vi.spyOn(userService, 'findOneById');
|
const spy_userService_findOneById = vi.spyOn(userService, 'findOneById');
|
||||||
const spy_userService_findOneByUsername = vi.spyOn(userService, 'findOneByUsername');
|
const spy_userService_findOneByUsername = vi.spyOn(userService, 'findOneByUsername');
|
||||||
const spy_userService_updateUser = vi.spyOn(userService, 'updateUser');
|
const spy_userService_updateUser = vi.spyOn(userService, 'updateUser');
|
||||||
await expect(
|
await expect(
|
||||||
service.updateProfile(faker.string.uuid(), {
|
service.updateProfile(faker.string.uuid(), {
|
||||||
username: faker.internet.userName(),
|
username: faker.internet.userName(),
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual({
|
).resolves.toEqual({
|
||||||
error: 'Username already in use',
|
error: 'Username already in use',
|
||||||
});
|
});
|
||||||
expect(spy_userService_findOneById).toBeCalledTimes(1);
|
expect(spy_userService_findOneById).toBeCalledTimes(1);
|
||||||
expect(spy_userService_findOneByUsername).toBeCalledTimes(1);
|
expect(spy_userService_findOneByUsername).toBeCalledTimes(1);
|
||||||
expect(spy_userService_updateUser).toBeCalledTimes(0);
|
expect(spy_userService_updateUser).toBeCalledTimes(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not error if the user id of new username is the current user id', async () => {
|
it('should not error if the user id of new username is the current user id', async () => {
|
||||||
userService.findOneById = vi.fn().mockResolvedValueOnce(dbUser);
|
userService.findOneById = vi.fn().mockResolvedValueOnce(dbUser);
|
||||||
userService.findOneByUsername = vi.fn().mockResolvedValue({
|
userService.findOneByUsername = vi.fn().mockResolvedValue({
|
||||||
id: dbUser.id,
|
id: dbUser.id,
|
||||||
});
|
});
|
||||||
userService.updateUser = vi.fn().mockResolvedValue(dbUser);
|
userService.updateUser = vi.fn().mockResolvedValue(dbUser);
|
||||||
|
|
||||||
const spy_userService_findOneById = vi.spyOn(userService, 'findOneById');
|
const spy_userService_findOneById = vi.spyOn(userService, 'findOneById');
|
||||||
const spy_userService_findOneByUsername = vi.spyOn(userService, 'findOneByUsername');
|
const spy_userService_findOneByUsername = vi.spyOn(userService, 'findOneByUsername');
|
||||||
const spy_userService_updateUser = vi.spyOn(userService, 'updateUser');
|
const spy_userService_updateUser = vi.spyOn(userService, 'updateUser');
|
||||||
await expect(
|
await expect(
|
||||||
service.updateProfile(dbUser.id, {
|
service.updateProfile(dbUser.id, {
|
||||||
username: dbUser.id,
|
username: dbUser.id,
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual(dbUser);
|
).resolves.toEqual(dbUser);
|
||||||
expect(spy_userService_findOneById).toBeCalledTimes(1);
|
expect(spy_userService_findOneById).toBeCalledTimes(1);
|
||||||
expect(spy_userService_findOneByUsername).toBeCalledTimes(1);
|
expect(spy_userService_findOneByUsername).toBeCalledTimes(1);
|
||||||
expect(spy_userService_updateUser).toBeCalledTimes(1);
|
expect(spy_userService_updateUser).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,77 +1,77 @@
|
||||||
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();
|
||||||
let service: UserRolesService;
|
let service: UserRolesService;
|
||||||
const userRolesRepository = vi.mocked(UserRolesRepository.prototype);
|
const userRolesRepository = vi.mocked(UserRolesRepository.prototype);
|
||||||
const rolesService = vi.mocked(RolesService.prototype);
|
const rolesService = vi.mocked(RolesService.prototype);
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
container
|
container
|
||||||
.bind<UserRolesRepository>({ provide: UserRolesRepository, useValue: userRolesRepository })
|
.bind<UserRolesRepository>({ provide: UserRolesRepository, useValue: userRolesRepository })
|
||||||
.bind<RolesService>({ provide: RolesService, useValue: rolesService });
|
.bind<RolesService>({ provide: RolesService, useValue: rolesService });
|
||||||
|
|
||||||
service = container.get(UserRolesService);
|
service = container.get(UserRolesService);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
vi.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
const timeStampDate = new Date();
|
const timeStampDate = new Date();
|
||||||
const roleUUID = faker.string.uuid();
|
const roleUUID = faker.string.uuid();
|
||||||
const userUUID = faker.string.uuid();
|
const userUUID = faker.string.uuid();
|
||||||
const dbRole = {
|
const dbRole = {
|
||||||
id: roleUUID,
|
id: roleUUID,
|
||||||
cuid: 'ciglo1j8q0000t9j4xq8d6p5e',
|
cuid: 'ciglo1j8q0000t9j4xq8d6p5e',
|
||||||
name: RoleName.ADMIN,
|
name: RoleName.ADMIN,
|
||||||
createdAt: timeStampDate,
|
createdAt: timeStampDate,
|
||||||
updatedAt: timeStampDate,
|
updatedAt: timeStampDate,
|
||||||
};
|
};
|
||||||
|
|
||||||
const dbUserRole = {
|
const dbUserRole = {
|
||||||
id: faker.string.uuid(),
|
id: faker.string.uuid(),
|
||||||
cuid: 'ciglo1j8q0000t9j4xq8d6p5e',
|
cuid: 'ciglo1j8q0000t9j4xq8d6p5e',
|
||||||
role_id: roleUUID,
|
role_id: roleUUID,
|
||||||
user_id: userUUID,
|
user_id: userUUID,
|
||||||
primary: true,
|
primary: true,
|
||||||
createdAt: timeStampDate,
|
createdAt: timeStampDate,
|
||||||
updatedAt: timeStampDate,
|
updatedAt: timeStampDate,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Create User Role', () => {
|
describe('Create User Role', () => {
|
||||||
it('should resolve', async () => {
|
it('should resolve', async () => {
|
||||||
rolesService.findOneByNameOrThrow = vi.fn().mockResolvedValue(dbRole satisfies Awaited<ReturnType<typeof rolesService.findOneByNameOrThrow>>);
|
rolesService.findOneByNameOrThrow = vi.fn().mockResolvedValue(dbRole satisfies Awaited<ReturnType<typeof rolesService.findOneByNameOrThrow>>);
|
||||||
|
|
||||||
userRolesRepository.create = vi.fn().mockResolvedValue(dbUserRole satisfies Awaited<ReturnType<typeof userRolesRepository.create>>);
|
userRolesRepository.create = vi.fn().mockResolvedValue(dbUserRole satisfies Awaited<ReturnType<typeof userRolesRepository.create>>);
|
||||||
|
|
||||||
const spy_rolesService_findOneByNameOrThrow = vi.spyOn(rolesService, 'findOneByNameOrThrow');
|
const spy_rolesService_findOneByNameOrThrow = vi.spyOn(rolesService, 'findOneByNameOrThrow');
|
||||||
const spy_userRolesRepository_create = vi.spyOn(userRolesRepository, 'create');
|
const spy_userRolesRepository_create = vi.spyOn(userRolesRepository, 'create');
|
||||||
|
|
||||||
await expect(service.addRoleToUser(userUUID, RoleName.ADMIN, true)).resolves.not.toThrowError();
|
await expect(service.addRoleToUser(userUUID, RoleName.ADMIN, true)).resolves.not.toThrowError();
|
||||||
expect(spy_rolesService_findOneByNameOrThrow).toBeCalledWith(RoleName.ADMIN);
|
expect(spy_rolesService_findOneByNameOrThrow).toBeCalledWith(RoleName.ADMIN);
|
||||||
expect(spy_rolesService_findOneByNameOrThrow).toBeCalledTimes(1);
|
expect(spy_rolesService_findOneByNameOrThrow).toBeCalledTimes(1);
|
||||||
expect(spy_userRolesRepository_create).toBeCalledWith({
|
expect(spy_userRolesRepository_create).toBeCalledWith({
|
||||||
user_id: userUUID,
|
user_id: userUUID,
|
||||||
role_id: dbRole.id,
|
role_id: dbRole.id,
|
||||||
primary: true,
|
primary: true,
|
||||||
});
|
});
|
||||||
expect(spy_userRolesRepository_create).toBeCalledTimes(1);
|
expect(spy_userRolesRepository_create).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
it('should error on no role found', async () => {
|
it('should error on no role found', async () => {
|
||||||
rolesService.findOneByNameOrThrow = vi.fn().mockResolvedValue(undefined);
|
rolesService.findOneByNameOrThrow = vi.fn().mockResolvedValue(undefined);
|
||||||
|
|
||||||
const spy_rolesService_findOneByNameOrThrow = vi.spyOn(rolesService, 'findOneByNameOrThrow');
|
const spy_rolesService_findOneByNameOrThrow = vi.spyOn(rolesService, 'findOneByNameOrThrow');
|
||||||
await expect(service.addRoleToUser(userUUID, RoleName.ADMIN, true)).rejects.toThrowError(`Role with name ${RoleName.ADMIN} not found`);
|
await expect(service.addRoleToUser(userUUID, RoleName.ADMIN, true)).rejects.toThrowError(`Role with name ${RoleName.ADMIN} not found`);
|
||||||
expect(spy_rolesService_findOneByNameOrThrow).toBeCalledWith(RoleName.ADMIN);
|
expect(spy_rolesService_findOneByNameOrThrow).toBeCalledWith(RoleName.ADMIN);
|
||||||
expect(spy_rolesService_findOneByNameOrThrow).toBeCalledTimes(1);
|
expect(spy_rolesService_findOneByNameOrThrow).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,143 +1,143 @@
|
||||||
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();
|
||||||
let service: UsersService;
|
let service: UsersService;
|
||||||
const credentialsRepository = vi.mocked(CredentialsRepository.prototype);
|
const credentialsRepository = vi.mocked(CredentialsRepository.prototype);
|
||||||
const drizzleService = vi.mocked(DrizzleService.prototype, { deep: true });
|
const drizzleService = vi.mocked(DrizzleService.prototype, { deep: true });
|
||||||
const tokensService = vi.mocked(TokensService.prototype);
|
const tokensService = vi.mocked(TokensService.prototype);
|
||||||
const usersRepository = vi.mocked(UsersRepository.prototype);
|
const usersRepository = vi.mocked(UsersRepository.prototype);
|
||||||
const userRolesService = vi.mocked(UserRolesService.prototype);
|
const userRolesService = vi.mocked(UserRolesService.prototype);
|
||||||
const wishlistsService = vi.mocked(WishlistsService.prototype);
|
const wishlistsService = vi.mocked(WishlistsService.prototype);
|
||||||
const collectionsService = vi.mocked(CollectionsService.prototype);
|
const collectionsService = vi.mocked(CollectionsService.prototype);
|
||||||
|
|
||||||
// Mocking the dependencies
|
// Mocking the dependencies
|
||||||
vi.mock('pg', () => ({
|
vi.mock('pg', () => ({
|
||||||
Pool: vi.fn().mockImplementation(() => ({
|
Pool: vi.fn().mockImplementation(() => ({
|
||||||
connect: vi.fn(),
|
connect: vi.fn(),
|
||||||
end: vi.fn(),
|
end: vi.fn(),
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('drizzle-orm/node-postgres', () => ({
|
vi.mock('drizzle-orm/node-postgres', () => ({
|
||||||
drizzle: vi.fn().mockImplementation(() => ({
|
drizzle: vi.fn().mockImplementation(() => ({
|
||||||
transaction: vi.fn().mockImplementation((callback) => callback()),
|
transaction: vi.fn().mockImplementation((callback) => callback()),
|
||||||
// Add other methods you need to mock
|
// Add other methods you need to mock
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
container
|
container
|
||||||
.bind<CredentialsRepository>({ provide: CredentialsRepository, useValue: credentialsRepository })
|
.bind<CredentialsRepository>({ provide: CredentialsRepository, useValue: credentialsRepository })
|
||||||
.bind<DrizzleService>({ provide: DrizzleService, useValue: drizzleService })
|
.bind<DrizzleService>({ provide: DrizzleService, useValue: drizzleService })
|
||||||
.bind<TokensService>({ provide: TokensService, useValue: tokensService })
|
.bind<TokensService>({ provide: TokensService, useValue: tokensService })
|
||||||
.bind<UsersRepository>({ provide: UsersRepository, useValue: usersRepository })
|
.bind<UsersRepository>({ provide: UsersRepository, useValue: usersRepository })
|
||||||
.bind<UserRolesService>({ provide: UserRolesService, useValue: userRolesService })
|
.bind<UserRolesService>({ provide: UserRolesService, useValue: userRolesService })
|
||||||
.bind<WishlistsService>({ provide: WishlistsService, useValue: wishlistsService })
|
.bind<WishlistsService>({ provide: WishlistsService, useValue: wishlistsService })
|
||||||
.bind<CollectionsService>({ provide: CollectionsService, useValue: collectionsService });
|
.bind<CollectionsService>({ provide: CollectionsService, useValue: collectionsService });
|
||||||
|
|
||||||
service = container.get(UsersService);
|
service = container.get(UsersService);
|
||||||
|
|
||||||
drizzleService.db = {
|
drizzleService.db = {
|
||||||
transaction: vi.fn().mockImplementation(async (callback) => {
|
transaction: vi.fn().mockImplementation(async (callback) => {
|
||||||
return await callback();
|
return await callback();
|
||||||
}),
|
}),
|
||||||
} as any;
|
} as any;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
vi.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
const timeStampDate = new Date();
|
const timeStampDate = new Date();
|
||||||
const dbUser = {
|
const dbUser = {
|
||||||
id: faker.string.uuid(),
|
id: faker.string.uuid(),
|
||||||
cuid: 'ciglo1j8q0000t9j4xq8d6p5e',
|
cuid: 'ciglo1j8q0000t9j4xq8d6p5e',
|
||||||
first_name: faker.person.firstName(),
|
first_name: faker.person.firstName(),
|
||||||
last_name: faker.person.lastName(),
|
last_name: faker.person.lastName(),
|
||||||
email: faker.internet.email(),
|
email: faker.internet.email(),
|
||||||
username: faker.internet.userName(),
|
username: faker.internet.userName(),
|
||||||
verified: false,
|
verified: false,
|
||||||
receive_email: false,
|
receive_email: false,
|
||||||
mfa_enabled: false,
|
mfa_enabled: false,
|
||||||
theme: 'system',
|
theme: 'system',
|
||||||
createdAt: timeStampDate,
|
createdAt: timeStampDate,
|
||||||
updatedAt: timeStampDate,
|
updatedAt: timeStampDate,
|
||||||
};
|
};
|
||||||
const dbCredentials = {
|
const dbCredentials = {
|
||||||
id: faker.string.uuid(),
|
id: faker.string.uuid(),
|
||||||
user_id: dbUser.id,
|
user_id: dbUser.id,
|
||||||
type: CredentialsType.PASSWORD,
|
type: CredentialsType.PASSWORD,
|
||||||
secret_data: 'hashedPassword',
|
secret_data: 'hashedPassword',
|
||||||
createdAt: timeStampDate,
|
createdAt: timeStampDate,
|
||||||
updatedAt: timeStampDate,
|
updatedAt: timeStampDate,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Create User', () => {
|
describe('Create User', () => {
|
||||||
it('should resolve', async () => {
|
it('should resolve', async () => {
|
||||||
const hashedPassword = 'testhash';
|
const hashedPassword = 'testhash';
|
||||||
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');
|
||||||
const createdUser = await service.create({
|
const createdUser = await service.create({
|
||||||
firstName: faker.person.firstName(),
|
firstName: faker.person.firstName(),
|
||||||
lastName: faker.person.lastName(),
|
lastName: faker.person.lastName(),
|
||||||
email: faker.internet.email(),
|
email: faker.internet.email(),
|
||||||
username: faker.internet.userName(),
|
username: faker.internet.userName(),
|
||||||
password: faker.string.alphanumeric(10),
|
password: faker.string.alphanumeric(10),
|
||||||
confirm_password: faker.string.alphanumeric(10),
|
confirm_password: faker.string.alphanumeric(10),
|
||||||
});
|
});
|
||||||
expect(createdUser).toEqual(dbUser);
|
expect(createdUser).toEqual(dbUser);
|
||||||
expect(spy_tokensService_createHashToken).toBeCalledTimes(1);
|
expect(spy_tokensService_createHashToken).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('Update User', () => {
|
describe('Update User', () => {
|
||||||
it('should resolve Password Exiting Credentials', async () => {
|
it('should resolve Password Exiting Credentials', async () => {
|
||||||
const hashedPassword = 'testhash';
|
const hashedPassword = 'testhash';
|
||||||
tokensService.createHashedToken = vi.fn().mockResolvedValue(hashedPassword);
|
tokensService.createHashedToken = vi.fn().mockResolvedValue(hashedPassword);
|
||||||
credentialsRepository.update = vi.fn().mockResolvedValue(dbCredentials satisfies Awaited<ReturnType<typeof credentialsRepository.update>>);
|
credentialsRepository.update = vi.fn().mockResolvedValue(dbCredentials satisfies Awaited<ReturnType<typeof credentialsRepository.update>>);
|
||||||
credentialsRepository.findPasswordCredentialsByUserId = vi
|
credentialsRepository.findPasswordCredentialsByUserId = vi
|
||||||
.fn()
|
.fn()
|
||||||
.mockResolvedValue(dbCredentials satisfies Awaited<ReturnType<typeof credentialsRepository.findPasswordCredentialsByUserId>>);
|
.mockResolvedValue(dbCredentials satisfies Awaited<ReturnType<typeof credentialsRepository.findPasswordCredentialsByUserId>>);
|
||||||
|
|
||||||
const spy_tokensService_createHashToken = vi.spyOn(tokensService, 'createHashedToken');
|
const spy_tokensService_createHashToken = vi.spyOn(tokensService, 'createHashedToken');
|
||||||
const spy_credentialsRepository_findPasswordCredentialsByUserId = vi.spyOn(credentialsRepository, 'findPasswordCredentialsByUserId');
|
const spy_credentialsRepository_findPasswordCredentialsByUserId = vi.spyOn(credentialsRepository, 'findPasswordCredentialsByUserId');
|
||||||
const spy_credentialsRepository_update = vi.spyOn(credentialsRepository, 'update');
|
const spy_credentialsRepository_update = vi.spyOn(credentialsRepository, 'update');
|
||||||
await expect(service.updatePassword(dbUser.id, faker.string.alphanumeric(10))).resolves.toBeUndefined();
|
await expect(service.updatePassword(dbUser.id, faker.string.alphanumeric(10))).resolves.toBeUndefined();
|
||||||
expect(spy_tokensService_createHashToken).toBeCalledTimes(1);
|
expect(spy_tokensService_createHashToken).toBeCalledTimes(1);
|
||||||
expect(spy_credentialsRepository_findPasswordCredentialsByUserId).toBeCalledTimes(1);
|
expect(spy_credentialsRepository_findPasswordCredentialsByUserId).toBeCalledTimes(1);
|
||||||
expect(spy_credentialsRepository_update).toBeCalledTimes(1);
|
expect(spy_credentialsRepository_update).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
it('Should Create User Password No Existing Credentials', async () => {
|
it('Should Create User Password No Existing Credentials', async () => {
|
||||||
const hashedPassword = 'testhash';
|
const hashedPassword = 'testhash';
|
||||||
tokensService.createHashedToken = vi.fn().mockResolvedValue(hashedPassword);
|
tokensService.createHashedToken = vi.fn().mockResolvedValue(hashedPassword);
|
||||||
credentialsRepository.findPasswordCredentialsByUserId = vi.fn().mockResolvedValue(null);
|
credentialsRepository.findPasswordCredentialsByUserId = vi.fn().mockResolvedValue(null);
|
||||||
credentialsRepository.create = vi.fn().mockResolvedValue(dbCredentials satisfies Awaited<ReturnType<typeof credentialsRepository.create>>);
|
credentialsRepository.create = vi.fn().mockResolvedValue(dbCredentials satisfies Awaited<ReturnType<typeof credentialsRepository.create>>);
|
||||||
|
|
||||||
const spy_tokensService_createHashToken = vi.spyOn(tokensService, 'createHashedToken');
|
const spy_tokensService_createHashToken = vi.spyOn(tokensService, 'createHashedToken');
|
||||||
const spy_credentialsRepository_create = vi.spyOn(credentialsRepository, 'create');
|
const spy_credentialsRepository_create = vi.spyOn(credentialsRepository, 'create');
|
||||||
const spy_credentialsRepository_findPasswordCredentialsByUserId = vi.spyOn(credentialsRepository, 'findPasswordCredentialsByUserId');
|
const spy_credentialsRepository_findPasswordCredentialsByUserId = vi.spyOn(credentialsRepository, 'findPasswordCredentialsByUserId');
|
||||||
|
|
||||||
await expect(service.updatePassword(dbUser.id, faker.string.alphanumeric(10))).resolves.not.toThrow();
|
await expect(service.updatePassword(dbUser.id, faker.string.alphanumeric(10))).resolves.not.toThrow();
|
||||||
expect(spy_tokensService_createHashToken).toBeCalledTimes(1);
|
expect(spy_tokensService_createHashToken).toBeCalledTimes(1);
|
||||||
expect(spy_credentialsRepository_findPasswordCredentialsByUserId).toBeCalledTimes(1);
|
expect(spy_credentialsRepository_findPasswordCredentialsByUserId).toBeCalledTimes(1);
|
||||||
expect(spy_credentialsRepository_create).toHaveBeenCalledTimes(1);
|
expect(spy_credentialsRepository_create).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
70
src/lib/server/api/users/credentials.repository.ts
Normal file
70
src/lib/server/api/users/credentials.repository.ts
Normal 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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/lib/server/api/users/dtos/update-user.dto.ts
Normal file
14
src/lib/server/api/users/dtos/update-user.dto.ts
Normal 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>;
|
||||||
23
src/lib/server/api/users/dtos/user.dto.ts
Normal file
23
src/lib/server/api/users/dtos/user.dto.ts
Normal 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>;
|
||||||
28
src/lib/server/api/users/federated_identity.repository.ts
Normal file
28
src/lib/server/api/users/federated_identity.repository.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/lib/server/api/users/recovery-codes.repository.ts
Normal file
32
src/lib/server/api/users/recovery-codes.repository.ts
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/lib/server/api/users/recovery-codes.service.ts
Normal file
40
src/lib/server/api/users/recovery-codes.service.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/lib/server/api/users/roles.repository.ts
Normal file
53
src/lib/server/api/users/roles.repository.ts
Normal 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
Loading…
Reference in a new issue