mirror of
https://github.com/BradNut/boredgame
synced 2025-09-08 17:40:22 +00:00
commit
8527783060
242 changed files with 13788 additions and 4028 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -7,6 +7,8 @@ node_modules
|
|||
.env.*
|
||||
*.xdp*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
.vercel
|
||||
.output
|
||||
.idea
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
20.6.1
|
||||
20
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
||||
|
|
|
|||
3
Caddyfile
Normal file
3
Caddyfile
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
boredgame.localhost {
|
||||
reverse_proxy / localhost:4173
|
||||
}
|
||||
55
biome.json
Normal file
55
biome.json
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"formatWithErrors": false,
|
||||
"indentStyle": "tab",
|
||||
"indentWidth": 2,
|
||||
"lineEnding": "lf",
|
||||
"lineWidth": 150,
|
||||
"attributePosition": "auto",
|
||||
"ignore": [
|
||||
"**/.DS_Store",
|
||||
"**/node_modules",
|
||||
"./build",
|
||||
"./.svelte-kit",
|
||||
"./package",
|
||||
"**/.env",
|
||||
"**/.env.*",
|
||||
"**/pnpm-lock.yaml",
|
||||
"**/package-lock.json",
|
||||
"**/yarn.lock"
|
||||
]
|
||||
},
|
||||
"organizeImports": { "enabled": true },
|
||||
"linter": { "enabled": true, "rules": { "recommended": true } },
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"jsxQuoteStyle": "double",
|
||||
"quoteProperties": "asNeeded",
|
||||
"trailingCommas": "all",
|
||||
"semicolons": "asNeeded",
|
||||
"arrowParentheses": "always",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"quoteStyle": "single",
|
||||
"attributePosition": "auto"
|
||||
},
|
||||
"parser": {
|
||||
"unsafeParameterDecoratorsEnabled": true
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"include": ["*.svelte"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
"useConst": "off",
|
||||
"useImportType": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -9,5 +9,6 @@
|
|||
"aliases": {
|
||||
"components": "$lib/components",
|
||||
"utils": "$lib/utils"
|
||||
}
|
||||
},
|
||||
"typescript": true
|
||||
}
|
||||
43
docker-compose.yaml
Normal file
43
docker-compose.yaml
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
services:
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: postgres
|
||||
ports:
|
||||
- '5432:5432'
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
redis:
|
||||
image: redis:latest
|
||||
ports:
|
||||
- '6379:6379'
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
# cerbos:
|
||||
# image: ghcr.io/cerbos/cerbos:0.38.1
|
||||
# environment:
|
||||
# CERBOS_NO_TELEMETRY: 1
|
||||
# ports:
|
||||
# - '3592:3592'
|
||||
# volumes:
|
||||
# - ./policies:/policies
|
||||
# caddy:
|
||||
# image: caddy:latest
|
||||
# restart: unless-stopped
|
||||
# ports:
|
||||
# - "80:80"
|
||||
# - "443:443"
|
||||
# - "443:443/udp"
|
||||
# volumes:
|
||||
# - ./Caddyfile:/etc/caddy/Caddyfile
|
||||
# - ./site:/srv
|
||||
# - caddy_data:/data
|
||||
# - caddy_config:/config
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
# policies_data:
|
||||
# caddy_data:
|
||||
# caddy_config:
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import 'dotenv/config';
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
import env from './src/env';
|
||||
import 'dotenv/config'
|
||||
import { defineConfig } from 'drizzle-kit'
|
||||
import env from './src/env'
|
||||
|
||||
export default defineConfig({
|
||||
dialect: 'postgresql',
|
||||
schema: './src/db/schema/index.ts',
|
||||
out: './src/db/migrations',
|
||||
out: './src/lib/server/api/databases/migrations',
|
||||
schema: './src/lib/server/api/databases/tables/drizzle.ts',
|
||||
dbCredentials: {
|
||||
host: env.DATABASE_HOST || 'localhost',
|
||||
port: Number(env.DATABASE_PORT) || 5432,
|
||||
|
|
@ -18,4 +18,8 @@ export default defineConfig({
|
|||
verbose: true,
|
||||
// Always as for confirmation
|
||||
strict: true,
|
||||
});
|
||||
migrations: {
|
||||
table: 'migrations',
|
||||
schema: 'public',
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { error, json } from '@sveltejs/kit';
|
||||
|
||||
export async function GET({ url, locals, params }) {
|
||||
const searchParams = Object.fromEntries(url.searchParams);
|
||||
return json({});
|
||||
}
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
|
||||
export async function GET({ url, locals, params }) {
|
||||
const searchParams = Object.fromEntries(url.searchParams);
|
||||
return json({});
|
||||
}
|
||||
61
oldApis/collection/[id]/search/+server.ts
Normal file
61
oldApis/collection/[id]/search/+server.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { collection_items, usersTable } from '$db/schema'
|
||||
import { error, json } from '@sveltejs/kit'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { db } from '../../../../src/lib/server/api/packages/drizzle'
|
||||
|
||||
// Search a user's collection
|
||||
export async function GET({ url, locals, params }) {
|
||||
const searchParams = Object.fromEntries(url.searchParams)
|
||||
const q = searchParams?.q || ''
|
||||
const limit = Number.parseInt(searchParams?.limit) || 10
|
||||
const skip = Number.parseInt(searchParams?.skip) || 0
|
||||
const order = searchParams?.order || 'asc'
|
||||
const sort = searchParams?.sort || 'name'
|
||||
const collection_id = params.id
|
||||
console.log('url', url)
|
||||
console.log('username', locals?.user?.id)
|
||||
|
||||
if (!locals.user) {
|
||||
error(401, { message: 'Unauthorized' })
|
||||
}
|
||||
|
||||
const collection = await db.query.collections.findFirst({
|
||||
where: eq(usersTable.id, locals?.user?.id),
|
||||
})
|
||||
console.log('collection', collection)
|
||||
|
||||
if (!collection) {
|
||||
console.log('Collection was not found')
|
||||
error(404, { message: 'Collection was not found' })
|
||||
}
|
||||
|
||||
try {
|
||||
const userCollectionItems = await db.query.collection_items.findMany({
|
||||
where: eq(collection_items.collection_id, collection_id),
|
||||
with: {
|
||||
game: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
thumb_url: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: (collection_items, { asc, desc }) => {
|
||||
const dbSort = sort === 'dateAdded' ? collection_items.created_at : collection_items.times_played
|
||||
if (order === 'asc') {
|
||||
return asc(dbSort)
|
||||
} else {
|
||||
return desc(dbSort)
|
||||
}
|
||||
},
|
||||
offset: skip,
|
||||
limit,
|
||||
})
|
||||
|
||||
return json(userCollectionItems)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
error(500, { message: 'Something went wrong' })
|
||||
}
|
||||
}
|
||||
34
oldApis/reset-password/+server.ts
Normal file
34
oldApis/reset-password/+server.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { PUBLIC_SITE_URL } from '$env/static/public'
|
||||
import { createPasswordResetToken } from '$lib/server/auth-utils.js'
|
||||
import { error } from '@sveltejs/kit'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { usersTable } from '../../src/lib/server/api/databases/tables'
|
||||
import { db } from '../../src/lib/server/api/packages/drizzle'
|
||||
|
||||
export async function POST({ locals, request }) {
|
||||
const { email }: { email: string } = await request.json()
|
||||
|
||||
if (!locals.user) {
|
||||
error(401, { message: 'Unauthorized' })
|
||||
}
|
||||
|
||||
const user = await db.query.usersTable.findFirst({
|
||||
where: eq(usersTable.email, email),
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
error(200, {
|
||||
message: 'Email sent! Please check your email for a link to reset your password.',
|
||||
})
|
||||
}
|
||||
|
||||
const verificationToken = await createPasswordResetToken(user.id)
|
||||
const verificationLink = PUBLIC_SITE_URL + verificationToken
|
||||
|
||||
// TODO: send email
|
||||
console.log('Verification link: ' + verificationLink)
|
||||
|
||||
return new Response(null, {
|
||||
status: 200,
|
||||
})
|
||||
}
|
||||
48
oldApis/reset-password/[token]/+server.ts
Normal file
48
oldApis/reset-password/[token]/+server.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { eq } from 'drizzle-orm'
|
||||
import { isWithinExpirationDate } from 'oslo'
|
||||
import { password_reset_tokens } from '../../../src/lib/server/api/databases/tables'
|
||||
// import { lucia } from '$lib/server/lucia';
|
||||
import { db } from '../../../src/lib/server/api/packages/drizzle'
|
||||
|
||||
export async function POST({ request, params }) {
|
||||
const { password } = await request.json()
|
||||
|
||||
if (typeof password !== 'string' || password.length < 8) {
|
||||
return new Response(null, {
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
|
||||
const verificationToken = params.token
|
||||
|
||||
const token = await db.query.password_reset_tokens.findFirst({
|
||||
where: eq(password_reset_tokens.id, verificationToken),
|
||||
})
|
||||
if (!token) {
|
||||
await db.delete(password_reset_tokens).where(eq(password_reset_tokens.id, verificationToken))
|
||||
return new Response(null, {
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
|
||||
if (!token?.expires_at || !isWithinExpirationDate(token.expires_at)) {
|
||||
return new Response(null, {
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
|
||||
// await lucia.invalidateUserSessions(token.user_id);
|
||||
// const hashPassword = await new Argon2id().hash(password);
|
||||
// // await db.update(usersTable).set({ hashed_password: hashPassword }).where(eq(usersTable.id, token.user_id));
|
||||
//
|
||||
// const session = await lucia.createSession(token.user_id, {});
|
||||
// const sessionCookie = lucia.createSessionCookie(session.id);
|
||||
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: '/',
|
||||
'Set-Cookie': sessionCookie.serialize(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { error, json } from '@sveltejs/kit';
|
||||
|
||||
export async function GET({ url, locals, params }) {
|
||||
const searchParams = Object.fromEntries(url.searchParams);
|
||||
return json({});
|
||||
}
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
|
||||
export async function GET({ url, locals, params }) {
|
||||
const searchParams = Object.fromEntries(url.searchParams);
|
||||
return json({});
|
||||
}
|
||||
106
package.json
106
package.json
|
|
@ -3,6 +3,11 @@
|
|||
"version": "0.0.2",
|
||||
"private": "true",
|
||||
"scripts": {
|
||||
"db:push": "drizzle-kit push",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "tsx src/lib/server/api/databases/migrate.ts",
|
||||
"db:seed": "tsx src/lib/server/api/databases/seed.ts",
|
||||
"db:studio": "drizzle-kit studio --verbose",
|
||||
"dev": "NODE_OPTIONS=\"--inspect\" vite dev --host",
|
||||
"build": "vite build",
|
||||
"package": "svelte-kit package",
|
||||
|
|
@ -11,107 +16,118 @@
|
|||
"test:ui": "svelte-kit sync && playwright test --ui",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"test:unit": "vitest",
|
||||
"initialize": "pnpm install && docker compose up --no-recreate -d && pnpm db:migrate && pnpm db:seed",
|
||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||
"format": "prettier --plugin-search-dir . --write .",
|
||||
"site:update": "pnpm update -i -L",
|
||||
"generate": "drizzle-kit generate",
|
||||
"migrate": "tsx src/db/migrate.ts",
|
||||
"seed": "tsx src/db/seed.ts",
|
||||
"push": "drizzle-kit push"
|
||||
"test:unit": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.8.3",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@melt-ui/pp": "^0.3.2",
|
||||
"@melt-ui/svelte": "^0.83.0",
|
||||
"@playwright/test": "^1.45.1",
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"@sveltejs/adapter-auto": "^3.2.2",
|
||||
"@sveltejs/enhanced-img": "^0.3.0",
|
||||
"@sveltejs/kit": "^2.5.18",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||
"@playwright/test": "^1.46.1",
|
||||
"@sveltejs/adapter-auto": "^3.2.4",
|
||||
"@sveltejs/enhanced-img": "^0.3.4",
|
||||
"@sveltejs/kit": "^2.5.25",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"@types/node": "^20.14.10",
|
||||
"@types/pg": "^8.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "^7.16.0",
|
||||
"@typescript-eslint/parser": "^7.16.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"drizzle-kit": "^0.23.0",
|
||||
"@types/node": "^20.16.3",
|
||||
"@types/pg": "^8.11.8",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"drizzle-kit": "^0.23.2",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.42.0",
|
||||
"eslint-plugin-svelte": "^2.43.0",
|
||||
"just-clone": "^6.2.0",
|
||||
"just-debounce-it": "^3.2.0",
|
||||
"postcss": "^8.4.39",
|
||||
"lucia": "3.2.0",
|
||||
"lucide-svelte": "^0.408.0",
|
||||
"nodemailer": "^6.9.15",
|
||||
"postcss": "^8.4.44",
|
||||
"postcss-import": "^16.1.0",
|
||||
"postcss-load-config": "^5.1.0",
|
||||
"postcss-preset-env": "^9.6.0",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-svelte": "^3.2.5",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
"sass": "^1.77.8",
|
||||
"satori": "^0.10.14",
|
||||
"satori-html": "^0.3.2",
|
||||
"svelte": "5.0.0-next.175",
|
||||
"svelte-check": "^3.8.4",
|
||||
"svelte-check": "^3.8.6",
|
||||
"svelte-headless-table": "^0.18.2",
|
||||
"svelte-meta-tags": "^3.1.2",
|
||||
"svelte-meta-tags": "^3.1.4",
|
||||
"svelte-preprocess": "^6.0.2",
|
||||
"svelte-sequential-preprocessor": "^2.0.1",
|
||||
"sveltekit-flash-message": "^2.4.4",
|
||||
"sveltekit-rate-limiter": "^0.5.1",
|
||||
"sveltekit-superforms": "^2.16.0",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"sveltekit-rate-limiter": "^0.5.2",
|
||||
"sveltekit-superforms": "^2.17.0",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.6.3",
|
||||
"tsx": "^4.16.2",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^5.3.3",
|
||||
"tslib": "^2.7.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.4.3",
|
||||
"vitest": "^1.6.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0 || >=20.0.0 <21.0.0",
|
||||
"pnpm": ">=8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/fira-mono": "^5.0.13",
|
||||
"@fontsource/fira-mono": "^5.0.14",
|
||||
"@hono/swagger-ui": "^0.4.1",
|
||||
"@hono/zod-openapi": "^0.15.3",
|
||||
"@hono/zod-validator": "^0.2.2",
|
||||
"@iconify-icons/line-md": "^1.2.30",
|
||||
"@iconify-icons/mdi": "^1.2.48",
|
||||
"@lucia-auth/adapter-drizzle": "^1.0.7",
|
||||
"@internationalized/date": "^3.5.5",
|
||||
"@lucia-auth/adapter-drizzle": "^1.1.0",
|
||||
"@lukeed/uuid": "^2.0.1",
|
||||
"@neondatabase/serverless": "^0.9.4",
|
||||
"@neondatabase/serverless": "^0.9.5",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@sveltejs/adapter-vercel": "^5.4.1",
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"@sveltejs/adapter-node": "^5.2.2",
|
||||
"@sveltejs/adapter-vercel": "^5.4.3",
|
||||
"@types/feather-icons": "^4.29.4",
|
||||
"@vercel/og": "^0.5.20",
|
||||
"bits-ui": "^0.21.12",
|
||||
"arctic": "^1.9.2",
|
||||
"bits-ui": "^0.21.13",
|
||||
"boardgamegeekclient": "^1.9.1",
|
||||
"bullmq": "^5.12.13",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cookie": "^0.6.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"dotenv-expand": "^11.0.6",
|
||||
"drizzle-orm": "^0.32.0",
|
||||
"drizzle-orm": "^0.32.2",
|
||||
"drizzle-zod": "^0.5.1",
|
||||
"feather-icons": "^4.29.2",
|
||||
"formsnap": "^1.0.1",
|
||||
"handlebars": "^4.7.8",
|
||||
"hono": "^4.5.11",
|
||||
"hono-rate-limiter": "^0.4.0",
|
||||
"html-entities": "^2.5.2",
|
||||
"iconify-icon": "^2.1.0",
|
||||
"ioredis": "^5.4.1",
|
||||
"just-capitalize": "^3.2.0",
|
||||
"just-kebab-case": "^4.2.0",
|
||||
"loader": "^2.1.1",
|
||||
"lucia": "3.2.0",
|
||||
"lucide-svelte": "^0.408.0",
|
||||
"open-props": "^1.7.5",
|
||||
"open-props": "^1.7.6",
|
||||
"oslo": "^1.2.1",
|
||||
"pg": "^8.12.0",
|
||||
"postgres": "^3.4.4",
|
||||
"qrcode": "^1.5.3",
|
||||
"qrcode": "^1.5.4",
|
||||
"radix-svelte": "^0.9.0",
|
||||
"rate-limit-redis": "^4.2.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"svelte-french-toast": "^1.2.0",
|
||||
"svelte-lazy-loader": "^1.0.0",
|
||||
"tailwind-merge": "^2.4.0",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwind-variants": "^0.2.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod-to-json-schema": "^3.23.1"
|
||||
"tsyringe": "^4.8.0",
|
||||
"zod-to-json-schema": "^3.23.2"
|
||||
}
|
||||
}
|
||||
2550
pnpm-lock.yaml
2550
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
8
policies/.cerbos.yaml
Normal file
8
policies/.cerbos.yaml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
server:
|
||||
httpListenAddr: ":3592"
|
||||
|
||||
storage:
|
||||
driver: "disk"
|
||||
disk:
|
||||
directory: /policies
|
||||
watchForChanges: true
|
||||
26
policies/ticket.yaml
Normal file
26
policies/ticket.yaml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
apiVersion: api.cerbos.dev/v1
|
||||
resourcePolicy:
|
||||
version: default
|
||||
resource: ticket
|
||||
|
||||
rules:
|
||||
- actions:
|
||||
- "*"
|
||||
effect: EFFECT_ALLOW
|
||||
roles:
|
||||
- admin
|
||||
- actions:
|
||||
- read
|
||||
- update
|
||||
effect: EFFECT_ALLOW
|
||||
roles:
|
||||
- customer
|
||||
condition:
|
||||
match:
|
||||
expr: request.resource.attr.cust_id == request.principal.id
|
||||
- actions:
|
||||
- create
|
||||
- delete
|
||||
effect: EFFECT_DENY
|
||||
roles:
|
||||
- customer
|
||||
43
policies/ticket_test.yaml
Normal file
43
policies/ticket_test.yaml
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
name: test ticket
|
||||
|
||||
principals:
|
||||
adminOne:
|
||||
id: admin_1
|
||||
roles:
|
||||
- admin
|
||||
customerOne:
|
||||
id: cust_1
|
||||
roles:
|
||||
- customer
|
||||
|
||||
resources:
|
||||
ticketOne:
|
||||
kind: ticket
|
||||
id: ticket_1
|
||||
attr:
|
||||
cust_id: cust_1
|
||||
|
||||
tests:
|
||||
- name: test ticket
|
||||
input:
|
||||
principals:
|
||||
- adminOne
|
||||
- customerOne
|
||||
resources:
|
||||
- ticketOne
|
||||
actions:
|
||||
- create
|
||||
- delete
|
||||
|
||||
expected:
|
||||
- principal: adminOne
|
||||
resource: ticketOne
|
||||
actions:
|
||||
create: EFFECT_ALLOW
|
||||
delete: EFFECT_ALLOW
|
||||
|
||||
- principal: customerOne
|
||||
resource: ticketOne
|
||||
actions:
|
||||
create: EFFECT_DENY
|
||||
delete: EFFECT_DENY
|
||||
26
src/app.d.ts
vendored
26
src/app.d.ts
vendored
|
|
@ -1,3 +1,7 @@
|
|||
import type { User } from 'lucia';
|
||||
import type { ApiClient } from '$lib/server/api';
|
||||
import type { parseApiResponse } from '$lib/utils/api';
|
||||
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
// and what to do when importing types
|
||||
|
|
@ -13,6 +17,10 @@ declare global {
|
|||
};
|
||||
}
|
||||
interface Locals {
|
||||
api: ApiClient['api'];
|
||||
parseApiResponse: typeof parseApiResponse;
|
||||
getAuthedUser: () => Promise<Returned<User> | null>;
|
||||
getAuthedUserOrThrow: () => Promise<Returned<User>>;
|
||||
auth: import('lucia').AuthRequest;
|
||||
user: import('lucia').User | null;
|
||||
session: import('lucia').Session | null;
|
||||
|
|
@ -33,22 +41,12 @@ declare global {
|
|||
|
||||
interface Document {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
startViewTransition: (callback: any) => void; // Add your custom property/method here
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
startViewTransition: (callback: never) => void; // Add your custom property/method here
|
||||
}
|
||||
}
|
||||
|
||||
// interface PageData {}
|
||||
// interface Error {}
|
||||
// interface Platform {}
|
||||
|
||||
// /// <reference types="lucia" />
|
||||
// declare global {
|
||||
// namespace Lucia {
|
||||
// type Auth = import('$lib/server/lucia').Auth;
|
||||
// type DatabaseUserAttributes = User;
|
||||
// type DatabaseSessionAttributes = {};
|
||||
// }
|
||||
// }
|
||||
|
||||
// THIS IS IMPORTANT!!!
|
||||
// biome-ignore lint/complexity/noUselessEmptyExport: <explanation>
|
||||
// biome-ignore lint/style/useExportType: <explanation>
|
||||
export {};
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import pg from 'pg';
|
||||
import env from '../env';
|
||||
import * as schema from './schema';
|
||||
|
||||
// create the connection
|
||||
export const pool = new pg.Pool({
|
||||
user: env.DATABASE_USER,
|
||||
password: env.DATABASE_PASSWORD,
|
||||
host: env.DATABASE_HOST,
|
||||
port: Number(env.DATABASE_PORT).valueOf(),
|
||||
database: env.DATABASE_DB,
|
||||
ssl: env.DATABASE_HOST !== 'localhost',
|
||||
max: env.DB_MIGRATING || env.DB_SEEDING ? 1 : undefined,
|
||||
});
|
||||
|
||||
export const db = drizzle(pool, {
|
||||
schema,
|
||||
logger: env.NODE_ENV === 'development',
|
||||
});
|
||||
|
||||
export type db = typeof db;
|
||||
|
||||
export default db;
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import 'dotenv/config';
|
||||
import postgres from 'postgres';
|
||||
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||
import { migrate } from 'drizzle-orm/postgres-js/migrator';
|
||||
import env from '../env';
|
||||
import config from '../../drizzle.config';
|
||||
|
||||
const connection = postgres({
|
||||
host: env.DATABASE_HOST || 'localhost',
|
||||
port: env.DATABASE_PORT,
|
||||
user: env.DATABASE_USER || 'root',
|
||||
password: env.DATABASE_PASSWORD || '',
|
||||
database: env.DATABASE_DB || 'boredgame',
|
||||
ssl: env.NODE_ENV === 'development' ? false : 'require',
|
||||
max: 1,
|
||||
});
|
||||
const db = drizzle(connection);
|
||||
|
||||
try {
|
||||
await migrate(db, { migrationsFolder: config.out! });
|
||||
console.log('Migrations complete');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
process.exit();
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
|
||||
import { createId as cuid2 } from '@paralleldrive/cuid2';
|
||||
import { type InferSelectModel, relations } from 'drizzle-orm';
|
||||
import categoriesToExternalIds from './categoriesToExternalIds';
|
||||
import categories_to_games from './categoriesToGames';
|
||||
import { timestamps } from '../utils';
|
||||
|
||||
const categories = pgTable('categories', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
cuid: text('cuid')
|
||||
.unique()
|
||||
.$defaultFn(() => cuid2()),
|
||||
name: text('name'),
|
||||
slug: text('slug'),
|
||||
...timestamps,
|
||||
});
|
||||
|
||||
export type Categories = InferSelectModel<typeof categories>;
|
||||
|
||||
export const categories_relations = relations(categories, ({ many }) => ({
|
||||
categories_to_games: many(categories_to_games),
|
||||
categoriesToExternalIds: many(categoriesToExternalIds),
|
||||
}));
|
||||
|
||||
export default categories;
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core';
|
||||
import categories from './categories';
|
||||
import externalIds from './externalIds';
|
||||
|
||||
const categoriesToExternalIds = pgTable(
|
||||
'categories_to_external_ids',
|
||||
{
|
||||
categoryId: uuid('category_id')
|
||||
.notNull()
|
||||
.references(() => categories.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
|
||||
externalId: uuid('external_id')
|
||||
.notNull()
|
||||
.references(() => externalIds.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
|
||||
},
|
||||
(table) => {
|
||||
return {
|
||||
categoriesToExternalIdsPkey: primaryKey({
|
||||
columns: [table.categoryId, table.externalId],
|
||||
}),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export default categoriesToExternalIds;
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
|
||||
import { createId as cuid2 } from '@paralleldrive/cuid2';
|
||||
import { type InferSelectModel, relations } from 'drizzle-orm';
|
||||
import users from './users';
|
||||
import { timestamps } from '../utils';
|
||||
|
||||
const collections = pgTable('collections', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
cuid: text('cuid')
|
||||
.unique()
|
||||
.$defaultFn(() => cuid2()),
|
||||
user_id: uuid('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
name: text('name').notNull().default('My Collection'),
|
||||
...timestamps,
|
||||
});
|
||||
|
||||
export const collection_relations = relations(collections, ({ one }) => ({
|
||||
user: one(users, {
|
||||
fields: [collections.user_id],
|
||||
references: [users.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export type Collections = InferSelectModel<typeof collections>;
|
||||
|
||||
export default collections;
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
export { default as users, user_relations, type Users } from './users';
|
||||
export { default as recoveryCodes, type RecoveryCodes } from './recoveryCodes';
|
||||
export {
|
||||
default as password_reset_tokens,
|
||||
password_reset_token_relations,
|
||||
type PasswordResetTokens,
|
||||
} from './passwordResetTokens';
|
||||
export { default as sessions, type Sessions } from './sessions';
|
||||
export { default as roles, role_relations, type Roles } from './roles';
|
||||
export { default as userRoles, user_role_relations, type UserRoles } from './userRoles';
|
||||
export { default as collections, collection_relations, type Collections } from './collections';
|
||||
export {
|
||||
default as collection_items,
|
||||
collection_item_relations,
|
||||
type CollectionItems,
|
||||
} from './collectionItems';
|
||||
export { default as wishlists, wishlists_relations, type Wishlists } from './wishlists';
|
||||
export {
|
||||
default as wishlist_items,
|
||||
wishlist_item_relations,
|
||||
type WishlistItems,
|
||||
} from './wishlistItems';
|
||||
export { default as externalIds, type ExternalIds, externalIdType } from './externalIds';
|
||||
export { default as games, gameRelations, type Games } from './games';
|
||||
export { default as gamesToExternalIds } from './gamesToExternalIds';
|
||||
export { default as expansions, expansion_relations, type Expansions } from './expansions';
|
||||
export { default as publishers, publishers_relations, type Publishers } from './publishers';
|
||||
export { default as publishers_to_games, publishers_to_games_relations } from './publishersToGames';
|
||||
export { default as publishersToExternalIds } from './publishersToExternalIds';
|
||||
export { default as categories, categories_relations, type Categories } from './categories';
|
||||
export { default as categoriesToExternalIds } from './categoriesToExternalIds';
|
||||
export { default as categories_to_games, categories_to_games_relations } from './categoriesToGames';
|
||||
export { default as mechanics, mechanics_relations, type Mechanics } from './mechanics';
|
||||
export { default as mechanicsToExternalIds } from './mechanicsToExternalIds';
|
||||
export { default as mechanics_to_games, mechanics_to_games_relations } from './mechanicsToGames';
|
||||
export { default as twoFactor } from './two-factor.table';
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
|
||||
import { createId as cuid2 } from '@paralleldrive/cuid2';
|
||||
import { type InferSelectModel, relations } from 'drizzle-orm';
|
||||
import mechanicsToGames from './mechanicsToGames';
|
||||
import mechanicsToExternalIds from './mechanicsToExternalIds';
|
||||
import { timestamps } from '../utils';
|
||||
|
||||
const mechanics = pgTable('mechanics', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
cuid: text('cuid')
|
||||
.unique()
|
||||
.$defaultFn(() => cuid2()),
|
||||
name: text('name'),
|
||||
slug: text('slug'),
|
||||
...timestamps,
|
||||
});
|
||||
|
||||
export type Mechanics = InferSelectModel<typeof mechanics>;
|
||||
|
||||
export const mechanics_relations = relations(mechanics, ({ many }) => ({
|
||||
mechanics_to_games: many(mechanicsToGames),
|
||||
mechanicsToExternalIds: many(mechanicsToExternalIds),
|
||||
}));
|
||||
|
||||
export default mechanics;
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
|
||||
import { createId as cuid2 } from '@paralleldrive/cuid2';
|
||||
import { type InferSelectModel, relations } from 'drizzle-orm';
|
||||
import users from './users';
|
||||
import { timestamps } from '../utils';
|
||||
|
||||
const password_reset_tokens = pgTable('password_reset_tokens', {
|
||||
id: text('id')
|
||||
.primaryKey()
|
||||
.$defaultFn(() => cuid2()),
|
||||
user_id: uuid('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
expires_at: timestamp('expires_at'),
|
||||
...timestamps,
|
||||
});
|
||||
|
||||
export type PasswordResetTokens = InferSelectModel<typeof password_reset_tokens>;
|
||||
|
||||
export const password_reset_token_relations = relations(password_reset_tokens, ({ one }) => ({
|
||||
user: one(users, {
|
||||
fields: [password_reset_tokens.user_id],
|
||||
references: [users.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export default password_reset_tokens;
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
|
||||
import { createId as cuid2 } from '@paralleldrive/cuid2';
|
||||
import { type InferSelectModel, relations } from 'drizzle-orm';
|
||||
import publishers_to_games from './publishersToGames';
|
||||
import publishersToExternalIds from './publishersToExternalIds';
|
||||
import { timestamps } from '../utils';
|
||||
|
||||
const publishers = pgTable('publishers', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
cuid: text('cuid')
|
||||
.unique()
|
||||
.$defaultFn(() => cuid2()),
|
||||
name: text('name'),
|
||||
slug: text('slug'),
|
||||
...timestamps,
|
||||
});
|
||||
|
||||
export type Publishers = InferSelectModel<typeof publishers>;
|
||||
|
||||
export const publishers_relations = relations(publishers, ({ many }) => ({
|
||||
publishers_to_games: many(publishers_to_games),
|
||||
publishersToExternalIds: many(publishersToExternalIds),
|
||||
}));
|
||||
|
||||
export default publishers;
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core';
|
||||
import type { InferSelectModel } from 'drizzle-orm';
|
||||
import users from './users';
|
||||
import { timestamps } from '../utils';
|
||||
|
||||
const recovery_codes = pgTable('recovery_codes', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
code: text('code').notNull(),
|
||||
used: boolean('used').default(false),
|
||||
...timestamps,
|
||||
});
|
||||
|
||||
export type RecoveryCodes = InferSelectModel<typeof recovery_codes>;
|
||||
|
||||
export default recovery_codes;
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
|
||||
import { createId as cuid2 } from '@paralleldrive/cuid2';
|
||||
import { type InferSelectModel, relations } from 'drizzle-orm';
|
||||
import user_roles from './userRoles';
|
||||
import { timestamps } from '../utils';
|
||||
|
||||
const roles = pgTable('roles', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
cuid: text('cuid')
|
||||
.unique()
|
||||
.$defaultFn(() => cuid2())
|
||||
.notNull(),
|
||||
name: text('name').unique().notNull(),
|
||||
...timestamps,
|
||||
});
|
||||
|
||||
export type Roles = InferSelectModel<typeof roles>;
|
||||
|
||||
export const role_relations = relations(roles, ({ many }) => ({
|
||||
user_roles: many(user_roles),
|
||||
}));
|
||||
|
||||
export default roles;
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
|
||||
import { createId as cuid2 } from '@paralleldrive/cuid2';
|
||||
import { type InferSelectModel, relations } from 'drizzle-orm';
|
||||
import users from './users';
|
||||
import { timestamps } from '../utils';
|
||||
|
||||
const wishlists = pgTable('wishlists', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
cuid: text('cuid')
|
||||
.unique()
|
||||
.$defaultFn(() => cuid2()),
|
||||
user_id: uuid('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
name: text('name').notNull().default('My Wishlist'),
|
||||
...timestamps,
|
||||
});
|
||||
|
||||
export type Wishlists = InferSelectModel<typeof wishlists>;
|
||||
|
||||
export const wishlists_relations = relations(wishlists, ({ one }) => ({
|
||||
user: one(users, {
|
||||
fields: [wishlists.user_id],
|
||||
references: [users.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export default wishlists;
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import { eq } from 'drizzle-orm';
|
||||
import { type db } from '$db';
|
||||
import * as schema from '$db/schema';
|
||||
import roles from './data/roles.json';
|
||||
|
||||
export default async function seed(db: db) {
|
||||
console.log('Creating roles ...');
|
||||
for (const role of roles) {
|
||||
await db.insert(schema.roles).values(role).onConflictDoNothing();
|
||||
}
|
||||
console.log('Roles created.');
|
||||
}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
import { eq } from 'drizzle-orm';
|
||||
import { Argon2id } from 'oslo/password';
|
||||
import { type db } from '$db';
|
||||
import * as schema from '$db/schema';
|
||||
import users from './data/users.json';
|
||||
import env from '../../env';
|
||||
|
||||
type JsonUser = {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
roles: {
|
||||
name: string;
|
||||
primary: boolean;
|
||||
}[];
|
||||
};
|
||||
|
||||
type JsonRole = {
|
||||
name: string;
|
||||
primary: boolean;
|
||||
};
|
||||
|
||||
export default async function seed(db: db) {
|
||||
const adminRole = await db.select().from(schema.roles).where(eq(schema.roles.name, 'admin'));
|
||||
const userRole = await db.select().from(schema.roles).where(eq(schema.roles.name, 'user'));
|
||||
|
||||
console.log('Admin Role: ', adminRole);
|
||||
const adminUser = await db
|
||||
.insert(schema.users)
|
||||
.values({
|
||||
username: `${env.ADMIN_USERNAME}`,
|
||||
email: '',
|
||||
hashed_password: await new Argon2id().hash(`${env.ADMIN_PASSWORD}`),
|
||||
first_name: 'Brad',
|
||||
last_name: 'S',
|
||||
verified: true,
|
||||
})
|
||||
.returning()
|
||||
.onConflictDoNothing();
|
||||
|
||||
console.log('Admin user created.', adminUser);
|
||||
|
||||
await db
|
||||
.insert(schema.collections)
|
||||
.values({ user_id: adminUser[0].id })
|
||||
.onConflictDoNothing();
|
||||
|
||||
await db
|
||||
.insert(schema.wishlists)
|
||||
.values({ user_id: adminUser[0].id })
|
||||
.onConflictDoNothing();
|
||||
|
||||
await db
|
||||
.insert(schema.userRoles)
|
||||
.values({
|
||||
user_id: adminUser[0].id,
|
||||
role_id: adminRole[0].id,
|
||||
})
|
||||
.onConflictDoNothing();
|
||||
|
||||
console.log('Admin user given admin role.');
|
||||
|
||||
await db
|
||||
.insert(schema.userRoles)
|
||||
.values({
|
||||
user_id: adminUser[0].id,
|
||||
role_id: userRole[0].id,
|
||||
})
|
||||
.onConflictDoNothing();
|
||||
|
||||
console.log('Admin user given user role.');
|
||||
await Promise.all(
|
||||
users.map(async (user) => {
|
||||
const [insertedUser] = await db
|
||||
.insert(schema.users)
|
||||
.values({
|
||||
...user,
|
||||
hashed_password: await new Argon2id().hash(user.password),
|
||||
})
|
||||
.returning();
|
||||
await db.insert(schema.collections).values({ user_id: insertedUser?.id });
|
||||
await db.insert(schema.wishlists).values({ user_id: insertedUser?.id });
|
||||
await Promise.all(
|
||||
user.roles.map(async (role: JsonRole) => {
|
||||
const foundRole = await db.query.roles.findFirst({
|
||||
where: eq(schema.roles.name, role.name),
|
||||
});
|
||||
await db.insert(schema.userRoles).values({
|
||||
user_id: insertedUser?.id,
|
||||
role_id: foundRole?.id,
|
||||
primary: role?.primary,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
// import { HTTPException } from 'hono/http-exception';
|
||||
import { timestamp } from 'drizzle-orm/pg-core';
|
||||
import { customType } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const citext = customType<{ data: string }>({
|
||||
dataType() {
|
||||
return 'citext';
|
||||
},
|
||||
});
|
||||
|
||||
export const cuid2 = customType<{ data: string }>({
|
||||
dataType() {
|
||||
return 'text';
|
||||
},
|
||||
});
|
||||
|
||||
export const takeFirst = <T>(values: T[]): T | null => {
|
||||
if (values.length === 0) return null;
|
||||
return values[0]!;
|
||||
};
|
||||
|
||||
export const takeFirstOrThrow = <T>(values: T[]): T => {
|
||||
if (values.length === 0)
|
||||
// throw new HTTPException(404, {
|
||||
// message: 'Resource not found',
|
||||
// });
|
||||
return values[0]!;
|
||||
};
|
||||
|
||||
export const timestamps = {
|
||||
createdAt: timestamp('created_at', {
|
||||
mode: 'date',
|
||||
withTimezone: true,
|
||||
})
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
updatedAt: timestamp('updated_at', {
|
||||
mode: 'date',
|
||||
withTimezone: true,
|
||||
})
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
};
|
||||
50
src/env.ts
50
src/env.ts
|
|
@ -1,52 +1,52 @@
|
|||
import { config } from 'dotenv';
|
||||
import { expand } from 'dotenv-expand';
|
||||
import { ZodError, z } from 'zod';
|
||||
import { config } from 'dotenv'
|
||||
import { expand } from 'dotenv-expand'
|
||||
import { ZodError, z } from 'zod'
|
||||
|
||||
const stringBoolean = z.coerce
|
||||
.string()
|
||||
.transform((val) => {
|
||||
return val === 'true';
|
||||
return val === 'true'
|
||||
})
|
||||
.default('false');
|
||||
.default('false')
|
||||
|
||||
const EnvSchema = z.object({
|
||||
NODE_ENV: z.string().default('development'),
|
||||
ADMIN_USERNAME: z.string(),
|
||||
ADMIN_PASSWORD: z.string(),
|
||||
DATABASE_USER: z.string(),
|
||||
DATABASE_PASSWORD: z.string(),
|
||||
DATABASE_HOST: z.string(),
|
||||
DATABASE_PORT: z.coerce.number(),
|
||||
DATABASE_DB: z.string(),
|
||||
DATABASE_URL: z.string(),
|
||||
DB_MIGRATING: stringBoolean,
|
||||
DB_SEEDING: stringBoolean,
|
||||
NODE_ENV: z.string().default('development'),
|
||||
ORIGIN: z.string(),
|
||||
PUBLIC_SITE_NAME: z.string(),
|
||||
PUBLIC_SITE_URL: z.string(),
|
||||
PUBLIC_UMAMI_DO_NOT_TRACK: z.string(),
|
||||
PUBLIC_UMAMI_ID: z.string(),
|
||||
PUBLIC_UMAMI_URL: z.string(),
|
||||
DB_MIGRATING: stringBoolean,
|
||||
DB_SEEDING: stringBoolean,
|
||||
ADMIN_USERNAME: z.string(),
|
||||
ADMIN_PASSWORD: z.string(),
|
||||
REDIS_URL: z.string(),
|
||||
TWO_FACTOR_TIMEOUT: z.coerce.number().default(300000),
|
||||
});
|
||||
})
|
||||
|
||||
export type EnvSchema = z.infer<typeof EnvSchema>;
|
||||
export type EnvSchema = z.infer<typeof EnvSchema>
|
||||
|
||||
expand(config());
|
||||
expand(config())
|
||||
|
||||
try {
|
||||
EnvSchema.parse(process.env);
|
||||
EnvSchema.parse(process.env)
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
let message = 'Missing required values in .env:\n';
|
||||
error.issues.forEach((issue) => {
|
||||
message += issue.path[0] + '\n';
|
||||
});
|
||||
const e = new Error(message);
|
||||
e.stack = '';
|
||||
throw e;
|
||||
} else {
|
||||
console.error(error);
|
||||
let message = 'Missing required values in .env:\n'
|
||||
for (const issue of error.issues) {
|
||||
message += `${issue.path[0]}\n`
|
||||
}
|
||||
const e = new Error(message)
|
||||
e.stack = ''
|
||||
throw e
|
||||
}
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
export default EnvSchema.parse(process.env);
|
||||
export default EnvSchema.parse(process.env)
|
||||
|
|
|
|||
|
|
@ -1,59 +1,42 @@
|
|||
// import * as Sentry from '@sentry/sveltekit';
|
||||
import { sequence } from '@sveltejs/kit/hooks';
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
import { dev } from '$app/environment';
|
||||
import { lucia } from '$lib/server/auth';
|
||||
import 'reflect-metadata'
|
||||
import { StatusCodes } from '$lib/constants/status-codes'
|
||||
import type { ApiRoutes } from '$lib/server/api'
|
||||
import { parseApiResponse } from '$lib/utils/api'
|
||||
import { type Handle, redirect } from '@sveltejs/kit'
|
||||
import { sequence } from '@sveltejs/kit/hooks'
|
||||
import { hc } from 'hono/client'
|
||||
|
||||
// TODO: Fix Sentry as it is not working on SvelteKit v2
|
||||
// Sentry.init({
|
||||
// dsn: 'https://742e43279df93a3c4a4a78c12eb1f879@o4506057768632320.ingest.sentry.io/4506057770401792',
|
||||
// tracesSampleRate: 1,
|
||||
// environment: dev ? 'development' : 'production',
|
||||
// enabled: !dev
|
||||
// });
|
||||
const apiClient: Handle = async ({ event, resolve }) => {
|
||||
/* ------------------------------ Register api ------------------------------ */
|
||||
const { api } = hc<ApiRoutes>('/', {
|
||||
fetch: event.fetch,
|
||||
headers: {
|
||||
'x-forwarded-for': event.url.host.includes('sveltekit-prerender') ? '127.0.0.1' : event.getClientAddress(),
|
||||
host: event.request.headers.get('host') || '',
|
||||
},
|
||||
})
|
||||
|
||||
export const authentication: Handle = async function ({ event, resolve }) {
|
||||
event.locals.startTimer = Date.now();
|
||||
|
||||
const ip = event.request.headers.get('x-forwarded-for') as string;
|
||||
const country = event.request.headers.get('x-vercel-ip-country') as string;
|
||||
event.locals.ip = dev ? '127.0.0.1' : ip; // || event.getClientAddress();
|
||||
event.locals.country = dev ? 'us' : country;
|
||||
|
||||
const sessionId = event.cookies.get(lucia.sessionCookieName);
|
||||
if (!sessionId) {
|
||||
event.locals.user = null;
|
||||
event.locals.session = null;
|
||||
return resolve(event);
|
||||
/* ----------------------------- Auth functions ----------------------------- */
|
||||
async function getAuthedUser() {
|
||||
const { data } = await api.user.$get().then(parseApiResponse)
|
||||
return data?.user
|
||||
}
|
||||
|
||||
const { session, user } = await lucia.validateSession(sessionId);
|
||||
if (session && session.fresh) {
|
||||
const sessionCookie = lucia.createSessionCookie(session.id);
|
||||
console.log('sessionCookie', JSON.stringify(sessionCookie, null, 2));
|
||||
// sveltekit types deviates from the de-facto standard, you can use 'as any' too
|
||||
event.cookies.set(sessionCookie.name, sessionCookie.value, {
|
||||
path: '.',
|
||||
...sessionCookie.attributes,
|
||||
});
|
||||
async function getAuthedUserOrThrow() {
|
||||
const { data } = await api.user.$get().then(parseApiResponse)
|
||||
if (!data || !data.user) throw redirect(StatusCodes.TEMPORARY_REDIRECT, '/')
|
||||
return data?.user
|
||||
}
|
||||
console.log('session from hooks', JSON.stringify(session, null, 2));
|
||||
if (!session) {
|
||||
const sessionCookie = lucia.createBlankSessionCookie();
|
||||
console.log('blank sessionCookie', JSON.stringify(sessionCookie, null, 2));
|
||||
event.cookies.set(sessionCookie.name, sessionCookie.value, {
|
||||
path: '.',
|
||||
...sessionCookie.attributes,
|
||||
});
|
||||
}
|
||||
event.locals.user = user;
|
||||
event.locals.session = session;
|
||||
|
||||
return resolve(event);
|
||||
};
|
||||
/* ------------------------------ Set contexts ------------------------------ */
|
||||
event.locals.api = api
|
||||
event.locals.parseApiResponse = parseApiResponse
|
||||
event.locals.getAuthedUser = getAuthedUser
|
||||
event.locals.getAuthedUserOrThrow = getAuthedUserOrThrow
|
||||
|
||||
export const handle: Handle = sequence(
|
||||
// Sentry.sentryHandle(),
|
||||
authentication,
|
||||
);
|
||||
// export const handleError = Sentry.handleErrorWithSentry();
|
||||
/* ----------------------------- Return response ---------------------------- */
|
||||
const response = await resolve(event)
|
||||
return response
|
||||
}
|
||||
|
||||
export const handle: Handle = sequence(apiClient)
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@
|
|||
</DropdownMenu.Root>
|
||||
{:else}
|
||||
<a href="/login"> <span class="flex-auto">Login</span></a>
|
||||
<a href="/sign-up"> <span class="flex-auto">Sign Up</span></a>
|
||||
<a href="/signup"> <span class="flex-auto">Sign Up</span></a>
|
||||
{/if}
|
||||
</nav>
|
||||
</header>
|
||||
|
|
|
|||
77
src/lib/components/LeftNav.svelte
Normal file
77
src/lib/components/LeftNav.svelte
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
|
||||
type Route = {
|
||||
href: string
|
||||
label: string
|
||||
}
|
||||
|
||||
let { children, routes }: { children: unknown; routes: Route[] } = $props()
|
||||
</script>
|
||||
|
||||
<div class="security-nav">
|
||||
<nav>
|
||||
<ul>
|
||||
{#each routes as { href, label }}
|
||||
<li>
|
||||
<a href={href} class:active={$page.url.pathname === href}>
|
||||
{label}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="security-nav-content">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.security-nav {
|
||||
display: flex;
|
||||
|
||||
nav {
|
||||
width: 16rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: #fff;
|
||||
padding: 1rem;
|
||||
border-right: 1px solid #ddd;
|
||||
height: 100vh;
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #337ab7;
|
||||
display: block;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
|
||||
&:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #23527c;
|
||||
font-weight: bold;
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.security-nav-content {
|
||||
flex: 1;
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
bind:value={pin}
|
||||
class={cn('flex items-center gap-2', rest.class)}
|
||||
type="text"
|
||||
placeholder=""
|
||||
placeholder="0"
|
||||
>
|
||||
{#each inputs as _}
|
||||
<PinInput.Input
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
// $: termsValue = $form.terms as Writable<boolean>;
|
||||
</script>
|
||||
|
||||
<form method="POST" action="/sign-up" use:enhance>
|
||||
<form method="POST" action="/signup" use:enhance>
|
||||
<h1>Signup user</h1>
|
||||
<label class="label">
|
||||
<span class="sr-only">First Name</span>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
{#if transition.type === 'stagger'}
|
||||
<div
|
||||
class="stagger transition"
|
||||
style:animation-duration="{transition.duration || 1 * 300}ms"
|
||||
style:animation-duration="{transition.duration || 300}ms"
|
||||
style:animation-delay="{transition.delay}ms"
|
||||
>
|
||||
<slot />
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ import type { Button as ButtonPrimitive } from "bits-ui";
|
|||
import Root from "./button.svelte";
|
||||
|
||||
const buttonVariants = tv({
|
||||
base: "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
base: "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
"border-input bg-background hover:bg-accent hover:text-accent-foreground border",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
</script>
|
||||
|
||||
<FormPrimitive.Description
|
||||
class={cn("text-sm text-muted-foreground", className)}
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...$$restProps}
|
||||
let:descriptionAttrs
|
||||
>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
</script>
|
||||
|
||||
<FormPrimitive.FieldErrors
|
||||
class={cn("text-sm font-medium text-destructive", className)}
|
||||
class={cn("text-destructive text-sm font-medium", className)}
|
||||
{...$$restProps}
|
||||
let:errors
|
||||
let:fieldErrorsAttrs
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
<FormPrimitive.Legend
|
||||
{...$$restProps}
|
||||
class={cn("text-sm font-medium leading-none data-[fs-error]:text-destructive", className)}
|
||||
class={cn("data-[fs-error]:text-destructive text-sm font-medium leading-none", className)}
|
||||
let:legendAttrs
|
||||
>
|
||||
<slot {legendAttrs} />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import Root from "./input.svelte";
|
||||
|
||||
type FormInputEvent<T extends Event = Event> = T & {
|
||||
export type FormInputEvent<T extends Event = Event> = T & {
|
||||
currentTarget: EventTarget & HTMLInputElement;
|
||||
};
|
||||
export type InputEvents = {
|
||||
|
|
@ -16,12 +16,14 @@ export type InputEvents = {
|
|||
mouseover: FormInputEvent<MouseEvent>;
|
||||
mouseenter: FormInputEvent<MouseEvent>;
|
||||
mouseleave: FormInputEvent<MouseEvent>;
|
||||
mousemove: FormInputEvent<MouseEvent>;
|
||||
paste: FormInputEvent<ClipboardEvent>;
|
||||
input: FormInputEvent<InputEvent>;
|
||||
wheel: FormInputEvent<WheelEvent>;
|
||||
};
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Input
|
||||
Root as Input,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import type { HTMLInputAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/utils";
|
||||
import type { InputEvents } from ".";
|
||||
import type { InputEvents } from "./index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
type $$Props = HTMLInputAttributes;
|
||||
type $$Events = InputEvents;
|
||||
|
|
@ -9,14 +9,19 @@
|
|||
let className: $$Props["class"] = undefined;
|
||||
export let value: $$Props["value"] = undefined;
|
||||
export { className as class };
|
||||
|
||||
// Workaround for https://github.com/sveltejs/svelte/issues/9305
|
||||
// Fixed in Svelte 5, but not backported to 4.x.
|
||||
export let readonly: $$Props["readonly"] = undefined;
|
||||
</script>
|
||||
|
||||
<input
|
||||
class={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-foreground file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
bind:value
|
||||
{readonly}
|
||||
on:blur
|
||||
on:change
|
||||
on:click
|
||||
|
|
@ -29,7 +34,9 @@
|
|||
on:mouseover
|
||||
on:mouseenter
|
||||
on:mouseleave
|
||||
on:mousemove
|
||||
on:paste
|
||||
on:input
|
||||
on:wheel|passive
|
||||
{...$$restProps}
|
||||
/>
|
||||
|
|
|
|||
357
src/lib/constants/status-codes.ts
Normal file
357
src/lib/constants/status-codes.ts
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
// 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
|
||||
}
|
||||
1
src/lib/dtos/create-user-role.dto.ts
Normal file
1
src/lib/dtos/create-user-role.dto.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
5
src/lib/dtos/id-params.dto.ts
Normal file
5
src/lib/dtos/id-params.dto.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const IdParamsDto = z.object({
|
||||
id: z.trim().number(),
|
||||
});
|
||||
20
src/lib/dtos/register-emailpassword.dto.ts
Normal file
20
src/lib/dtos/register-emailpassword.dto.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { z } from "zod";
|
||||
import { refinePasswords } from "$lib/validations/account";
|
||||
|
||||
export const registerEmailPasswordDto = z.object({
|
||||
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' }),
|
||||
password: z.string({ required_error: 'Password is required' }).trim(),
|
||||
confirm_password: z.string({ required_error: 'Confirm Password is required' }).trim(),
|
||||
})
|
||||
.superRefine(({ confirm_password, password }, ctx) => {
|
||||
refinePasswords(confirm_password, password, ctx);
|
||||
});
|
||||
|
||||
export type RegisterEmailPasswordDto = z.infer<typeof registerEmailPasswordDto>;
|
||||
12
src/lib/dtos/signin-username.dto.ts
Normal file
12
src/lib/dtos/signin-username.dto.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const signinUsernameDto = z.object({
|
||||
username: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(3, { message: 'Must be at least 3 characters' })
|
||||
.max(50, { message: 'Must be less than 50 characters' }),
|
||||
password: z.string({ required_error: 'Password is required' }).trim(),
|
||||
});
|
||||
|
||||
export type SigninUsernameDto = z.infer<typeof signinUsernameDto>;
|
||||
24
src/lib/dtos/signup-username-email.dto.ts
Normal file
24
src/lib/dtos/signup-username-email.dto.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { z } from "zod";
|
||||
import { refinePasswords } from "$lib/validations/account";
|
||||
|
||||
export const signupUsernameEmailDto = z.object({
|
||||
firstName: z.string().trim().optional(),
|
||||
lastName: z.string().trim().optional(),
|
||||
email: z.string()
|
||||
.trim()
|
||||
.max(64, {message: 'Email must be less than 64 characters'})
|
||||
.email({message: 'Please enter a valid email'})
|
||||
.optional(),
|
||||
username: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(3, {message: 'Must be at least 3 characters'})
|
||||
.max(50, {message: 'Must be less than 50 characters'}),
|
||||
password: z.string({required_error: 'Password is required'}).trim(),
|
||||
confirm_password: z.string({required_error: 'Confirm Password is required'}).trim()
|
||||
})
|
||||
.superRefine(({ confirm_password, password }, ctx) => {
|
||||
return refinePasswords(confirm_password, password, ctx);
|
||||
});
|
||||
|
||||
export type SignupUsernameEmailDto = z.infer<typeof signupUsernameEmailDto>
|
||||
11
src/lib/dtos/update-email.dto.ts
Normal file
11
src/lib/dtos/update-email.dto.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const updateEmailDto = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(64, {message: 'Email must be less than 64 characters'})
|
||||
.email({message: 'Please enter a valid email'})
|
||||
});
|
||||
|
||||
export type UpdateEmailDto = z.infer<typeof updateEmailDto>;
|
||||
23
src/lib/dtos/update-profile.dto.ts
Normal file
23
src/lib/dtos/update-profile.dto.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const updateProfileDto = z.object({
|
||||
firstName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(3, {message: 'Must be at least 3 characters'})
|
||||
.max(50, {message: 'Must be less than 50 characters'})
|
||||
.optional(),
|
||||
lastName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(3, {message: 'Must be at least 3 characters'})
|
||||
.max(50, {message: 'Must be less than 50 characters'})
|
||||
.optional(),
|
||||
username: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(3, {message: 'Must be at least 3 characters'})
|
||||
.max(50, {message: 'Must be less than 50 characters'})
|
||||
});
|
||||
|
||||
export type UpdateProfileDto = z.infer<typeof updateProfileDto>;
|
||||
7
src/lib/dtos/verify-password.dto.ts
Normal file
7
src/lib/dtos/verify-password.dto.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { z } from 'zod'
|
||||
|
||||
export const verifyPasswordDto = z.object({
|
||||
password: z.string({ required_error: 'Password is required' }).trim(),
|
||||
})
|
||||
|
||||
export type VerifyPasswordDto = z.infer<typeof verifyPasswordDto>
|
||||
11
src/lib/dtos/verify-totp.dto.ts
Normal file
11
src/lib/dtos/verify-totp.dto.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const verifyTotpDto = z.object({
|
||||
code: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(6, { message: 'Must be at least 6 characters' })
|
||||
.max(6, { message: 'Must be less than 6 characters' }),
|
||||
});
|
||||
|
||||
export type VerifyTotpDto = z.infer<typeof verifyTotpDto>;
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
</script>
|
||||
|
||||
<div class="progress" class:visible style:--progress={progress}>
|
||||
<div class="track"></div>
|
||||
<div class="track" />
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
|
|
|
|||
26
src/lib/server/api/common/exceptions.ts
Normal file
26
src/lib/server/api/common/exceptions.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { StatusCodes } from '$lib/constants/status-codes';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
|
||||
export function TooManyRequests(message: string = 'Too many requests') {
|
||||
return new HTTPException(StatusCodes.TOO_MANY_REQUESTS, { message });
|
||||
}
|
||||
|
||||
export function Forbidden(message: string = 'Forbidden') {
|
||||
return new HTTPException(StatusCodes.FORBIDDEN, { message });
|
||||
}
|
||||
|
||||
export function Unauthorized(message: string = 'Unauthorized') {
|
||||
return new HTTPException(StatusCodes.UNAUTHORIZED, { message });
|
||||
}
|
||||
|
||||
export function NotFound(message: string = 'Not Found') {
|
||||
return new HTTPException(StatusCodes.NOT_FOUND, { message });
|
||||
}
|
||||
|
||||
export function BadRequest(message: string = 'Bad Request') {
|
||||
return new HTTPException(StatusCodes.BAD_REQUEST, { message });
|
||||
}
|
||||
|
||||
export function InternalError(message: string = 'Internal Error') {
|
||||
return new HTTPException(StatusCodes.INTERNAL_SERVER_ERROR, { message });
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { Hono } from 'hono'
|
||||
import type { BlankSchema } from 'hono/types'
|
||||
import type { HonoTypes } from '../../types'
|
||||
|
||||
export interface Controller {
|
||||
controller: Hono<HonoTypes, BlankSchema, '/'>
|
||||
routes(): any
|
||||
}
|
||||
4
src/lib/server/api/common/interfaces/email.interface.ts
Normal file
4
src/lib/server/api/common/interfaces/email.interface.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export interface Email {
|
||||
subject(): string
|
||||
html(): string
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import type { DatabaseProvider } from '$lib/server/api/providers/database.provider'
|
||||
|
||||
export interface Repository {
|
||||
trxHost(trx: DatabaseProvider): any
|
||||
}
|
||||
14
src/lib/server/api/common/utils/repository.utils.ts
Normal file
14
src/lib/server/api/common/utils/repository.utils.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { HTTPException } from 'hono/http-exception'
|
||||
|
||||
export const takeFirst = <T>(values: T[]): T | null => {
|
||||
if (values.length === 0) return null
|
||||
return values[0] as T
|
||||
}
|
||||
|
||||
export const takeFirstOrThrow = <T>(values: T[]): T => {
|
||||
if (values.length === 0)
|
||||
throw new HTTPException(404, {
|
||||
message: 'Resource not found',
|
||||
})
|
||||
return values[0] as T
|
||||
}
|
||||
29
src/lib/server/api/common/utils/table.utils.ts
Normal file
29
src/lib/server/api/common/utils/table.utils.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { timestamp } from 'drizzle-orm/pg-core'
|
||||
import { customType } from 'drizzle-orm/pg-core'
|
||||
|
||||
export const citext = customType<{ data: string }>({
|
||||
dataType() {
|
||||
return 'citext'
|
||||
},
|
||||
})
|
||||
|
||||
export const cuid2 = customType<{ data: string }>({
|
||||
dataType() {
|
||||
return 'text'
|
||||
},
|
||||
})
|
||||
|
||||
export const timestamps = {
|
||||
createdAt: timestamp('created_at', {
|
||||
mode: 'date',
|
||||
withTimezone: true,
|
||||
})
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
updatedAt: timestamp('updated_at', {
|
||||
mode: 'date',
|
||||
withTimezone: true,
|
||||
})
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
}
|
||||
15
src/lib/server/api/configs/config.ts
Normal file
15
src/lib/server/api/configs/config.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import env from '../../../../env';
|
||||
|
||||
const isPreview = process.env.VERCEL_ENV === 'preview' || process.env.VERCEL_ENV === 'development';
|
||||
|
||||
let domain: string;
|
||||
if (process.env.NODE_ENV === 'production' || process.env.VERCEL_ENV === 'production') {
|
||||
domain = 'boredgame.vercel.app';
|
||||
} else if (isPreview && process.env.VERCEL_BRANCH_URL !== undefined) {
|
||||
domain = process.env.VERCEL_BRANCH_URL;
|
||||
} else {
|
||||
domain = 'localhost';
|
||||
}
|
||||
|
||||
export const config = { ...env, isProduction: process.env.NODE_ENV === 'production'
|
||||
|| process.env.VERCEL_ENV === 'production', domain };
|
||||
29
src/lib/server/api/controllers/collection.controller.ts
Normal file
29
src/lib/server/api/controllers/collection.controller.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import 'reflect-metadata'
|
||||
import type { Controller } from '$lib/server/api/common/interfaces/controller.interface'
|
||||
import { CollectionsService } from '$lib/server/api/services/collections.service'
|
||||
import { Hono } from 'hono'
|
||||
import { inject, injectable } from 'tsyringe'
|
||||
import { requireAuth } from '../middleware/auth.middleware'
|
||||
import type { HonoTypes } from '../types'
|
||||
|
||||
@injectable()
|
||||
export class CollectionController implements Controller {
|
||||
controller = new Hono<HonoTypes>()
|
||||
|
||||
constructor(@inject(CollectionsService) private readonly collectionsService: CollectionsService) {}
|
||||
|
||||
routes() {
|
||||
return this.controller
|
||||
.get('/', requireAuth, async (c) => {
|
||||
const user = c.var.user
|
||||
const collections = await this.collectionsService.findAllByUserId(user.id)
|
||||
console.log('collections service', collections)
|
||||
return c.json({ collections })
|
||||
})
|
||||
.get('/:cuid', requireAuth, async (c) => {
|
||||
const cuid = c.req.param('cuid')
|
||||
const collection = await this.collectionsService.findOneByCuid(cuid)
|
||||
return c.json({ collection })
|
||||
})
|
||||
}
|
||||
}
|
||||
75
src/lib/server/api/controllers/iam.controller.ts
Normal file
75
src/lib/server/api/controllers/iam.controller.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { StatusCodes } from '$lib/constants/status-codes'
|
||||
import type { Controller } from '$lib/server/api/common/interfaces/controller.interface'
|
||||
import { updateEmailDto } from '$lib/server/api/dtos/update-email.dto'
|
||||
import { updateProfileDto } from '$lib/server/api/dtos/update-profile.dto'
|
||||
import { verifyPasswordDto } from '$lib/server/api/dtos/verify-password.dto'
|
||||
import { limiter } from '$lib/server/api/middleware/rate-limiter.middleware'
|
||||
import { LuciaProvider } from '$lib/server/api/providers/lucia.provider'
|
||||
import { IamService } from '$lib/server/api/services/iam.service'
|
||||
import { zValidator } from '@hono/zod-validator'
|
||||
import { Hono } from 'hono'
|
||||
import { setCookie } from 'hono/cookie'
|
||||
import { inject, injectable } from 'tsyringe'
|
||||
import { requireAuth } from '../middleware/auth.middleware'
|
||||
import type { HonoTypes } from '../types'
|
||||
|
||||
@injectable()
|
||||
export class IamController implements Controller {
|
||||
controller = new Hono<HonoTypes>()
|
||||
|
||||
constructor(
|
||||
@inject(IamService) private readonly iamService: IamService,
|
||||
@inject(LuciaProvider) private lucia: LuciaProvider,
|
||||
) {}
|
||||
|
||||
routes() {
|
||||
return this.controller
|
||||
.get('/', requireAuth, async (c) => {
|
||||
const user = c.var.user
|
||||
return c.json({ user })
|
||||
})
|
||||
.put('/update/profile', requireAuth, zValidator('json', updateProfileDto), limiter({ limit: 30, minutes: 60 }), async (c) => {
|
||||
const user = c.var.user
|
||||
const { firstName, lastName, username } = c.req.valid('json')
|
||||
const updatedUser = await this.iamService.updateProfile(user.id, { firstName, lastName, username })
|
||||
if (!updatedUser) {
|
||||
return c.json('Username already in use', StatusCodes.BAD_REQUEST)
|
||||
}
|
||||
return c.json({ user: updatedUser }, StatusCodes.OK)
|
||||
})
|
||||
.post('/verify/password', requireAuth, zValidator('json', verifyPasswordDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
|
||||
const user = c.var.user
|
||||
const { password } = c.req.valid('json')
|
||||
const passwordVerified = await this.iamService.verifyPassword(user.id, { password })
|
||||
if (!passwordVerified) {
|
||||
console.log('Incorrect password')
|
||||
return c.json('Incorrect password', StatusCodes.BAD_REQUEST)
|
||||
}
|
||||
return c.json({}, StatusCodes.OK)
|
||||
})
|
||||
.post('/update/email', requireAuth, zValidator('json', updateEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
|
||||
const user = c.var.user
|
||||
const { email } = c.req.valid('json')
|
||||
const updatedUser = await this.iamService.updateEmail(user.id, { email })
|
||||
if (!updatedUser) {
|
||||
return c.json('Email already in use', StatusCodes.BAD_REQUEST)
|
||||
}
|
||||
return c.json({ user: updatedUser }, StatusCodes.OK)
|
||||
})
|
||||
.post('/logout', requireAuth, async (c) => {
|
||||
const sessionId = c.var.session.id
|
||||
await this.iamService.logout(sessionId)
|
||||
const sessionCookie = this.lucia.createBlankSessionCookie()
|
||||
setCookie(c, sessionCookie.name, sessionCookie.value, {
|
||||
path: sessionCookie.attributes.path,
|
||||
maxAge: sessionCookie.attributes.maxAge,
|
||||
domain: sessionCookie.attributes.domain,
|
||||
sameSite: sessionCookie.attributes.sameSite as any,
|
||||
secure: sessionCookie.attributes.secure,
|
||||
httpOnly: sessionCookie.attributes.httpOnly,
|
||||
expires: sessionCookie.attributes.expires,
|
||||
})
|
||||
return c.json({ status: 'success' })
|
||||
})
|
||||
}
|
||||
}
|
||||
44
src/lib/server/api/controllers/login.controller.ts
Normal file
44
src/lib/server/api/controllers/login.controller.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import 'reflect-metadata'
|
||||
import type { Controller } from '$lib/server/api/common/interfaces/controller.interface'
|
||||
import { signinUsernameDto } from '$lib/server/api/dtos/signin-username.dto'
|
||||
import { LuciaProvider } from '$lib/server/api/providers/lucia.provider'
|
||||
import { zValidator } from '@hono/zod-validator'
|
||||
import { Hono } from 'hono'
|
||||
import { setCookie } from 'hono/cookie'
|
||||
import { TimeSpan } from 'oslo'
|
||||
import { inject, injectable } from 'tsyringe'
|
||||
import { limiter } from '../middleware/rate-limiter.middleware'
|
||||
import { LoginRequestsService } from '../services/loginrequest.service'
|
||||
import type { HonoTypes } from '../types'
|
||||
|
||||
@injectable()
|
||||
export class LoginController implements Controller {
|
||||
controller = new Hono<HonoTypes>()
|
||||
|
||||
constructor(
|
||||
@inject(LoginRequestsService) private readonly loginRequestsService: LoginRequestsService,
|
||||
@inject(LuciaProvider) private lucia: LuciaProvider,
|
||||
) {}
|
||||
|
||||
routes() {
|
||||
return this.controller.post('/', zValidator('json', signinUsernameDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
|
||||
const { username, password } = c.req.valid('json')
|
||||
const session = await this.loginRequestsService.verify({ username, password }, c.req)
|
||||
const sessionCookie = this.lucia.createSessionCookie(session.id)
|
||||
console.log('set cookie', sessionCookie)
|
||||
setCookie(c, sessionCookie.name, sessionCookie.value, {
|
||||
path: sessionCookie.attributes.path,
|
||||
maxAge:
|
||||
sessionCookie?.attributes?.maxAge && sessionCookie?.attributes?.maxAge < new TimeSpan(365, 'd').seconds()
|
||||
? sessionCookie.attributes.maxAge
|
||||
: new TimeSpan(2, 'w').seconds(),
|
||||
domain: sessionCookie.attributes.domain,
|
||||
sameSite: sessionCookie.attributes.sameSite as any,
|
||||
secure: sessionCookie.attributes.secure,
|
||||
httpOnly: sessionCookie.attributes.httpOnly,
|
||||
expires: sessionCookie.attributes.expires,
|
||||
})
|
||||
return c.json({ message: 'ok' })
|
||||
})
|
||||
}
|
||||
}
|
||||
77
src/lib/server/api/controllers/mfa.controller.ts
Normal file
77
src/lib/server/api/controllers/mfa.controller.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import 'reflect-metadata'
|
||||
import { StatusCodes } from '$lib/constants/status-codes'
|
||||
import type { Controller } from '$lib/server/api/common/interfaces/controller.interface'
|
||||
import { verifyTotpDto } from '$lib/server/api/dtos/verify-totp.dto'
|
||||
import { db } from '$lib/server/api/packages/drizzle'
|
||||
import { RecoveryCodesService } from '$lib/server/api/services/recovery-codes.service'
|
||||
import { TotpService } from '$lib/server/api/services/totp.service'
|
||||
import { UsersService } from '$lib/server/api/services/users.service'
|
||||
import { zValidator } from '@hono/zod-validator'
|
||||
import { Hono } from 'hono'
|
||||
import { inject, injectable } from 'tsyringe'
|
||||
import { CredentialsType } from '../databases/tables'
|
||||
import { requireAuth } from '../middleware/auth.middleware'
|
||||
import type { HonoTypes } from '../types'
|
||||
|
||||
@injectable()
|
||||
export class MfaController implements Controller {
|
||||
controller = new Hono<HonoTypes>()
|
||||
|
||||
constructor(
|
||||
@inject(RecoveryCodesService) private readonly recoveryCodesService: RecoveryCodesService,
|
||||
@inject(TotpService) private readonly totpService: TotpService,
|
||||
@inject(UsersService) private readonly usersService: UsersService,
|
||||
) {}
|
||||
|
||||
routes() {
|
||||
return this.controller
|
||||
.get('/totp', requireAuth, async (c) => {
|
||||
const user = c.var.user
|
||||
const totpCredential = await this.totpService.findOneByUserId(user.id)
|
||||
return c.json({ totpCredential })
|
||||
})
|
||||
.post('/totp', requireAuth, async (c) => {
|
||||
const user = c.var.user
|
||||
const totpCredential = await this.totpService.create(user.id)
|
||||
return c.json({ totpCredential })
|
||||
})
|
||||
.delete('/totp', requireAuth, async (c) => {
|
||||
const user = c.var.user
|
||||
try {
|
||||
await this.totpService.deleteOneByUserIdAndType(user.id, CredentialsType.TOTP)
|
||||
await this.recoveryCodesService.deleteAllRecoveryCodesByUserId(user.id)
|
||||
await this.usersService.updateUser(user.id, { mfa_enabled: false })
|
||||
console.log('TOTP deleted')
|
||||
return c.body(null, StatusCodes.NO_CONTENT)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return c.status(StatusCodes.INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
})
|
||||
.get('/totp/recoveryCodes', requireAuth, async (c) => {
|
||||
const user = c.var.user
|
||||
// You can only view recovery codes once and that is on creation
|
||||
const existingCodes = await this.recoveryCodesService.findAllRecoveryCodesByUserId(user.id)
|
||||
if (existingCodes) {
|
||||
return c.body('You have already generated recovery codes', StatusCodes.BAD_REQUEST)
|
||||
}
|
||||
const recoveryCodes = await this.recoveryCodesService.createRecoveryCodes(user.id)
|
||||
return c.json({ recoveryCodes })
|
||||
})
|
||||
.post('/totp/verify', requireAuth, zValidator('json', verifyTotpDto), async (c) => {
|
||||
try {
|
||||
const user = c.var.user
|
||||
const { code } = c.req.valid('json')
|
||||
const verified = await this.totpService.verify(user.id, code)
|
||||
if (verified) {
|
||||
await this.usersService.updateUser(user.id, { mfa_enabled: true })
|
||||
return c.json({}, StatusCodes.OK)
|
||||
}
|
||||
return c.json('Invalid code', StatusCodes.BAD_REQUEST)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return c.status(StatusCodes.INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
58
src/lib/server/api/controllers/signup.controller.ts
Normal file
58
src/lib/server/api/controllers/signup.controller.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import 'reflect-metadata'
|
||||
import type { Controller } from '$lib/server/api/common/interfaces/controller.interface'
|
||||
import { signupUsernameEmailDto } from '$lib/server/api/dtos/signup-username-email.dto'
|
||||
import { limiter } from '$lib/server/api/middleware/rate-limiter.middleware'
|
||||
import { LuciaProvider } from '$lib/server/api/providers/lucia.provider'
|
||||
import { LoginRequestsService } from '$lib/server/api/services/loginrequest.service'
|
||||
import { UsersService } from '$lib/server/api/services/users.service'
|
||||
import { zValidator } from '@hono/zod-validator'
|
||||
import { Hono } from 'hono'
|
||||
import { setCookie } from 'hono/cookie'
|
||||
import { TimeSpan } from 'oslo'
|
||||
import { inject, injectable } from 'tsyringe'
|
||||
import type { HonoTypes } from '../types'
|
||||
|
||||
@injectable()
|
||||
export class SignupController implements Controller {
|
||||
controller = new Hono<HonoTypes>()
|
||||
|
||||
constructor(
|
||||
@inject(UsersService) private readonly usersService: UsersService,
|
||||
@inject(LoginRequestsService) private readonly loginRequestService: LoginRequestsService,
|
||||
@inject(LuciaProvider) private lucia: LuciaProvider,
|
||||
) {}
|
||||
|
||||
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, undefined)
|
||||
const sessionCookie = this.lucia.createSessionCookie(session.id)
|
||||
console.log('set cookie', sessionCookie)
|
||||
setCookie(c, sessionCookie.name, sessionCookie.value, {
|
||||
path: sessionCookie.attributes.path,
|
||||
maxAge:
|
||||
sessionCookie?.attributes?.maxAge && sessionCookie?.attributes?.maxAge < new TimeSpan(365, 'd').seconds()
|
||||
? sessionCookie.attributes.maxAge
|
||||
: new TimeSpan(2, 'w').seconds(),
|
||||
domain: sessionCookie.attributes.domain,
|
||||
sameSite: sessionCookie.attributes.sameSite as any,
|
||||
secure: sessionCookie.attributes.secure,
|
||||
httpOnly: sessionCookie.attributes.httpOnly,
|
||||
expires: sessionCookie.attributes.expires,
|
||||
})
|
||||
return c.json({ message: 'ok' })
|
||||
})
|
||||
}
|
||||
}
|
||||
32
src/lib/server/api/controllers/user.controller.ts
Normal file
32
src/lib/server/api/controllers/user.controller.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import 'reflect-metadata'
|
||||
import type { Controller } from '$lib/server/api/common/interfaces/controller.interface'
|
||||
import { UsersService } from '$lib/server/api/services/users.service'
|
||||
import { Hono } from 'hono'
|
||||
import { inject, injectable } from 'tsyringe'
|
||||
import { requireAuth } from '../middleware/auth.middleware'
|
||||
import type { HonoTypes } from '../types'
|
||||
|
||||
@injectable()
|
||||
export class UserController implements Controller {
|
||||
controller = new Hono<HonoTypes>()
|
||||
|
||||
constructor(@inject(UsersService) private readonly usersService: UsersService) {}
|
||||
|
||||
routes() {
|
||||
return this.controller
|
||||
.get('/', async (c) => {
|
||||
const user = c.var.user
|
||||
return c.json({ user })
|
||||
})
|
||||
.get('/:id', requireAuth, async (c) => {
|
||||
const id = c.req.param('id')
|
||||
const user = await this.usersService.findOneById(id)
|
||||
return c.json({ user })
|
||||
})
|
||||
.get('/username/:userName', requireAuth, async (c) => {
|
||||
const userName = c.req.param('userName')
|
||||
const user = await this.usersService.findOneByUsername(userName)
|
||||
return c.json({ user })
|
||||
})
|
||||
}
|
||||
}
|
||||
28
src/lib/server/api/controllers/wishlist.controller.ts
Normal file
28
src/lib/server/api/controllers/wishlist.controller.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import 'reflect-metadata'
|
||||
import type { Controller } from '$lib/server/api/common/interfaces/controller.interface'
|
||||
import { WishlistsService } from '$lib/server/api/services/wishlists.service'
|
||||
import { Hono } from 'hono'
|
||||
import { inject, injectable } from 'tsyringe'
|
||||
import { requireAuth } from '../middleware/auth.middleware'
|
||||
import type { HonoTypes } from '../types'
|
||||
|
||||
@injectable()
|
||||
export class WishlistController implements Controller {
|
||||
controller = new Hono<HonoTypes>()
|
||||
|
||||
constructor(@inject(WishlistsService) private readonly wishlistsService: WishlistsService) {}
|
||||
|
||||
routes() {
|
||||
return this.controller
|
||||
.get('/', requireAuth, async (c) => {
|
||||
const user = c.var.user
|
||||
const wishlists = await this.wishlistsService.findAllByUserId(user.id)
|
||||
return c.json({ wishlists })
|
||||
})
|
||||
.get('/:cuid', requireAuth, async (c) => {
|
||||
const cuid = c.req.param('cuid')
|
||||
const wishlist = await this.wishlistsService.findOneByCuid(cuid)
|
||||
return c.json({ wishlist })
|
||||
})
|
||||
}
|
||||
}
|
||||
26
src/lib/server/api/databases/migrate.ts
Normal file
26
src/lib/server/api/databases/migrate.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import 'dotenv/config'
|
||||
import { drizzle } from 'drizzle-orm/postgres-js'
|
||||
import { migrate } from 'drizzle-orm/postgres-js/migrator'
|
||||
import postgres from 'postgres'
|
||||
import config from '../../../../../drizzle.config'
|
||||
import env from '../../../../env'
|
||||
|
||||
const connection = postgres({
|
||||
host: env.DATABASE_HOST || 'localhost',
|
||||
port: env.DATABASE_PORT,
|
||||
user: env.DATABASE_USER || 'root',
|
||||
password: env.DATABASE_PASSWORD || '',
|
||||
database: env.DATABASE_DB || 'boredgame',
|
||||
ssl: env.NODE_ENV === 'development' ? false : 'require',
|
||||
max: 1,
|
||||
})
|
||||
const db = drizzle(connection)
|
||||
|
||||
try {
|
||||
await migrate(db, { migrationsFolder: config.out! })
|
||||
console.log('Migrations complete')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
process.exit()
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
CREATE TABLE IF NOT EXISTS "credentials" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"type" text DEFAULT 'password' NOT NULL,
|
||||
"secret_data" text NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "federated_identity" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"identity_provider" text NOT NULL,
|
||||
"federated_user_id" text NOT NULL,
|
||||
"federated_username" text NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "credentials" ADD CONSTRAINT "credentials_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "federated_identity" ADD CONSTRAINT "federated_identity_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "users" DROP COLUMN IF EXISTS "hashed_password";
|
||||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "users" ADD COLUMN "enabled" boolean DEFAULT false NOT NULL;
|
||||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "users" RENAME COLUMN "enabled" TO "mfa_enabled";
|
||||
1856
src/lib/server/api/databases/migrations/meta/0003_snapshot.json
Normal file
1856
src/lib/server/api/databases/migrations/meta/0003_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
1863
src/lib/server/api/databases/migrations/meta/0004_snapshot.json
Normal file
1863
src/lib/server/api/databases/migrations/meta/0004_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
1863
src/lib/server/api/databases/migrations/meta/0005_snapshot.json
Normal file
1863
src/lib/server/api/databases/migrations/meta/0005_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -22,6 +22,27 @@
|
|||
"when": 1720626020902,
|
||||
"tag": "0002_fancy_valkyrie",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "7",
|
||||
"when": 1723593488634,
|
||||
"tag": "0003_worried_taskmaster",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 4,
|
||||
"version": "7",
|
||||
"when": 1725055403926,
|
||||
"tag": "0004_heavy_sphinx",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 5,
|
||||
"version": "7",
|
||||
"when": 1725055643756,
|
||||
"tag": "0005_true_mathemanic",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
24
src/lib/server/api/databases/schemas/collections.schema.ts
Normal file
24
src/lib/server/api/databases/schemas/collections.schema.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { collections } from '$lib/server/api/databases/tables'
|
||||
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
|
||||
import type { z } from 'zod'
|
||||
|
||||
export const InsertCollectionSchema = createInsertSchema(collections, {
|
||||
name: (schema) =>
|
||||
schema.name.trim().min(3, { message: 'Must be at least 3 characters' }).max(64, { message: 'Must be less than 64 characters' }).optional(),
|
||||
}).omit({
|
||||
id: true,
|
||||
cuid: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
})
|
||||
|
||||
export type InsertCollectionSchema = z.infer<typeof InsertCollectionSchema>
|
||||
|
||||
export const SelectCollectionSchema = createSelectSchema(collections).omit({
|
||||
id: true,
|
||||
user_id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
})
|
||||
|
||||
export type SelectUserSchema = z.infer<typeof SelectCollectionSchema>
|
||||
24
src/lib/server/api/databases/schemas/users.schemas.ts
Normal file
24
src/lib/server/api/databases/schemas/users.schemas.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { usersTable } from '$lib/server/api/databases/tables'
|
||||
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
|
||||
import type { z } from 'zod'
|
||||
|
||||
export const InsertUserSchema = createInsertSchema(usersTable, {
|
||||
email: (schema) => schema.email.max(64).email().optional(),
|
||||
username: (schema) =>
|
||||
schema.username.min(3, { message: 'Must be at least 3 characters' }).max(50, { message: 'Must be less than 50 characters' }).optional(),
|
||||
first_name: (schema) =>
|
||||
schema.first_name.trim().min(3, { message: 'Must be at least 3 characters' }).max(64, { message: 'Must be less than 64 characters' }).optional(),
|
||||
last_name: (schema) =>
|
||||
schema.last_name.trim().min(3, { message: 'Must be at least 3 characters' }).max(64, { message: 'Must be less than 64 characters' }).optional(),
|
||||
}).omit({
|
||||
id: true,
|
||||
cuid: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
})
|
||||
|
||||
export type InsertUserSchema = z.infer<typeof InsertUserSchema>
|
||||
|
||||
export const SelectUserSchema = createSelectSchema(usersTable)
|
||||
|
||||
export type SelectUserSchema = z.infer<typeof SelectUserSchema>
|
||||
|
|
@ -1,25 +1,27 @@
|
|||
import { Table, getTableName, sql } from 'drizzle-orm';
|
||||
import env from '../env';
|
||||
import { db, pool } from '$db';
|
||||
import * as schema from './schema';
|
||||
import * as seeds from './seeds';
|
||||
import { Table, getTableName, sql } from 'drizzle-orm'
|
||||
import env from '../../../../env'
|
||||
import { db, pool } from '../packages/drizzle'
|
||||
import * as seeds from './seeds'
|
||||
import * as schema from './tables'
|
||||
|
||||
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: db, 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 [
|
||||
schema.categories,
|
||||
schema.categoriesToExternalIds,
|
||||
schema.categories_to_games,
|
||||
schema.categoriesTable,
|
||||
schema.categoriesToExternalIdsTable,
|
||||
schema.categories_to_games_table,
|
||||
schema.collection_items,
|
||||
schema.collections,
|
||||
schema.credentialsTable,
|
||||
schema.expansions,
|
||||
schema.externalIds,
|
||||
schema.federatedIdentityTable,
|
||||
schema.games,
|
||||
schema.gamesToExternalIds,
|
||||
schema.mechanics,
|
||||
|
|
@ -29,21 +31,21 @@ for (const table of [
|
|||
schema.publishers,
|
||||
schema.publishersToExternalIds,
|
||||
schema.publishers_to_games,
|
||||
schema.recoveryCodes,
|
||||
schema.recoveryCodesTable,
|
||||
schema.roles,
|
||||
schema.sessions,
|
||||
schema.userRoles,
|
||||
schema.users,
|
||||
schema.twoFactor,
|
||||
schema.wishlists,
|
||||
schema.sessionsTable,
|
||||
schema.twoFactorTable,
|
||||
schema.user_roles,
|
||||
schema.usersTable,
|
||||
schema.wishlist_items,
|
||||
schema.wishlists,
|
||||
]) {
|
||||
// await db.delete(table); // clear tables without truncating / resetting ids
|
||||
await resetTable(db, table);
|
||||
await resetTable(db, table)
|
||||
}
|
||||
|
||||
await seeds.roles(db);
|
||||
await seeds.users(db);
|
||||
await seeds.roles(db)
|
||||
await seeds.users(db)
|
||||
|
||||
await pool.end();
|
||||
process.exit();
|
||||
await pool.end()
|
||||
process.exit()
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue