Merge pull request #32 from BradNut/hono-zod-openapi

OpenAPI, Update Drizzle, Remove Lucia Auth
This commit is contained in:
Bradley Shellnut 2024-11-08 11:59:18 -08:00 committed by GitHub
commit eddb896378
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
132 changed files with 8821 additions and 2541 deletions

View file

@ -1,11 +1,12 @@
import 'dotenv/config'
import env from './src/lib/server/api/common/env'
import { defineConfig } from 'drizzle-kit'
import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';
import env from './src/lib/server/api/common/env';
export default defineConfig({
dialect: 'postgresql',
out: './src/lib/server/api/databases/migrations',
schema: './src/lib/server/api/databases/tables/index.ts',
out: './src/lib/server/api/databases/postgres/migrations',
schema: './src/lib/server/api/databases/postgres/tables/index.ts',
casing: 'snake_case',
dbCredentials: {
host: env.DATABASE_HOST || 'localhost',
port: Number(env.DATABASE_PORT) || 5432,
@ -22,4 +23,4 @@ export default defineConfig({
table: 'migrations',
schema: 'public',
},
})
});

View file

@ -1,34 +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'
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/postgres/tables';
import { db } from '../../src/lib/server/api/packages/drizzle';
export async function POST({ locals, request }) {
const { email }: { email: string } = await request.json()
const { email }: { email: string } = await request.json();
if (!locals.user) {
error(401, { message: 'Unauthorized' })
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
const verificationToken = await createPasswordResetToken(user.id);
const verificationLink = PUBLIC_SITE_URL + verificationToken;
// TODO: send email
console.log('Verification link: ' + verificationLink)
console.log('Verification link: ' + verificationLink);
return new Response(null, {
status: 200,
})
});
}

View file

@ -1,34 +1,34 @@
import { eq } from 'drizzle-orm'
import { isWithinExpirationDate } from 'oslo'
import { password_reset_tokens } from '../../../src/lib/server/api/databases/tables'
import { eq } from 'drizzle-orm';
import { isWithinExpirationDate } from 'oslo';
import { password_reset_tokens } from '../../../src/lib/server/api/databases/postgres/tables';
// import { lucia } from '$lib/server/lucia';
import { db } from '../../../src/lib/server/api/packages/drizzle'
import { db } from '../../../src/lib/server/api/packages/drizzle';
export async function POST({ request, params }) {
const { password } = await request.json()
const { password } = await request.json();
if (typeof password !== 'string' || password.length < 8) {
return new Response(null, {
status: 400,
})
});
}
const verificationToken = params.token
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))
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);
@ -44,5 +44,5 @@ export async function POST({ request, params }) {
Location: '/',
'Set-Cookie': sessionCookie.serialize(),
},
})
});
}

View file

@ -5,8 +5,8 @@
"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:migrate": "tsx src/lib/server/api/databases/postgres/migrate.ts",
"db:seed": "tsx src/lib/server/api/databases/postgres/seed.ts",
"db:studio": "drizzle-kit studio --verbose",
"dev": "NODE_OPTIONS=\"--inspect\" vite dev --host",
"build": "vite build",
@ -27,20 +27,20 @@
"@faker-js/faker": "^8.4.1",
"@melt-ui/pp": "^0.3.2",
"@melt-ui/svelte": "^0.83.0",
"@playwright/test": "^1.47.1",
"@sveltejs/adapter-auto": "^3.2.5",
"@sveltejs/enhanced-img": "^0.3.8",
"@sveltejs/kit": "^2.6.3",
"@playwright/test": "^1.48.2",
"@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/enhanced-img": "^0.3.10",
"@sveltejs/kit": "^2.8.0",
"@sveltejs/vite-plugin-svelte": "4.0.0-next.7",
"@types/cookie": "^0.6.0",
"@types/node": "^20.16.11",
"@types/node": "^20.17.6",
"@types/pg": "^8.11.10",
"@types/qrcode": "^1.5.5",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"arctic": "^1.9.2",
"autoprefixer": "^10.4.20",
"drizzle-kit": "^0.23.2",
"drizzle-kit": "^0.27.2",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "2.36.0-next.13",
@ -48,7 +48,7 @@
"just-debounce-it": "^3.2.0",
"lucia": "3.2.0",
"lucide-svelte": "^0.408.0",
"nodemailer": "^6.9.15",
"nodemailer": "^6.9.16",
"postcss": "^8.4.47",
"postcss-import": "^16.1.0",
"postcss-load-config": "^5.1.0",
@ -57,18 +57,18 @@
"prettier-plugin-svelte": "^3.2.7",
"svelte": "5.0.0-next.175",
"svelte-check": "^3.8.6",
"svelte-headless-table": "^0.18.2",
"svelte-headless-table": "^0.18.3",
"svelte-meta-tags": "^3.1.4",
"svelte-preprocess": "^6.0.3",
"svelte-sequential-preprocessor": "^2.0.2",
"sveltekit-flash-message": "^2.4.4",
"sveltekit-superforms": "^2.19.1",
"tailwindcss": "^3.4.13",
"sveltekit-superforms": "^2.20.0",
"tailwindcss": "^3.4.14",
"ts-node": "^10.9.2",
"tslib": "^2.7.0",
"tsx": "^4.19.1",
"typescript": "^5.6.2",
"vite": "^5.4.8",
"tslib": "^2.8.1",
"tsx": "^4.19.2",
"typescript": "^5.6.3",
"vite": "^5.4.10",
"vitest": "^1.6.0",
"zod": "^3.23.8"
},
@ -83,6 +83,7 @@
"@internationalized/date": "^3.5.6",
"@lucia-auth/adapter-drizzle": "^1.1.0",
"@lukeed/uuid": "^2.0.1",
"@needle-di/core": "^0.8.4",
"@neondatabase/serverless": "^0.9.5",
"@node-rs/argon2": "^1.8.3",
"@oslojs/crypto": "^1.0.1",
@ -92,27 +93,27 @@
"@oslojs/otp": "^1.0.0",
"@oslojs/webauthn": "^1.0.0",
"@paralleldrive/cuid2": "^2.2.2",
"@scalar/hono-api-reference": "^0.5.152",
"@sveltejs/adapter-node": "^5.2.5",
"@sveltejs/adapter-vercel": "^5.4.5",
"@scalar/hono-api-reference": "^0.5.159",
"@sveltejs/adapter-node": "^5.2.9",
"@sveltejs/adapter-vercel": "^5.4.7",
"@types/feather-icons": "^4.29.4",
"bits-ui": "^0.21.16",
"boardgamegeekclient": "^1.9.1",
"bullmq": "^5.17.1",
"bullmq": "^5.25.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cookie": "^0.6.0",
"cookie": "^1.0.1",
"dotenv": "^16.4.5",
"dotenv-expand": "^11.0.6",
"drizzle-orm": "^0.32.2",
"drizzle-orm": "^0.36.1",
"drizzle-zod": "^0.5.1",
"feather-icons": "^4.29.2",
"formsnap": "^1.0.1",
"handlebars": "^4.7.8",
"hono": "^4.6.3",
"hono": "^4.6.9",
"hono-pino": "^0.3.0",
"hono-rate-limiter": "^0.4.0",
"hono-zod-openapi": "^0.2.0",
"hono-zod-openapi": "^0.4.2",
"html-entities": "^2.5.2",
"iconify-icon": "^2.1.0",
"ioredis": "^5.4.1",
@ -120,23 +121,23 @@
"just-kebab-case": "^4.2.0",
"loader": "^2.1.1",
"mode-watcher": "^0.4.1",
"open-props": "^1.7.6",
"open-props": "^1.7.7",
"oslo": "^1.2.1",
"pg": "^8.13.0",
"pino": "^9.4.0",
"pino-pretty": "^11.2.2",
"postgres": "^3.4.4",
"pg": "^8.13.1",
"pino": "^9.5.0",
"pino-pretty": "^11.3.0",
"postgres": "^3.4.5",
"qrcode": "^1.5.4",
"radix-svelte": "^0.9.0",
"rate-limit-redis": "^4.2.0",
"reflect-metadata": "^0.2.2",
"stoker": "^1.0.9",
"stoker": "^1.3.0",
"svelte-lazy-loader": "^1.0.0",
"svelte-sonner": "^0.3.28",
"tailwind-merge": "^2.5.2",
"tailwind-merge": "^2.5.4",
"tailwind-variants": "^0.2.1",
"tailwindcss-animate": "^1.0.7",
"tsyringe": "^4.8.0",
"zod-to-json-schema": "^3.23.3"
"zod-to-json-schema": "^3.23.5"
}
}

File diff suppressed because it is too large Load diff

View file

@ -18,5 +18,7 @@ export const config: Config = {
database: env.DATABASE_DB,
ssl: false, // env.DATABASE_HOST !== 'localhost',
max: env.DB_MIGRATING || env.DB_SEEDING ? 1 : undefined,
migrating: env.DB_MIGRATING,
seeding: env.DB_SEEDING,
},
};

View file

@ -0,0 +1,12 @@
import { type HonoOpenApiOperation, type HonoOpenApiRequestSchemas, defineOpenApiOperation } from "hono-zod-openapi";
export const taggedAuthRoute = <T extends HonoOpenApiRequestSchemas>(
tag: string,
doc: HonoOpenApiOperation<T>,
) => {
return defineOpenApiOperation({
...doc,
tags: [tag],
security: [{ cookieAuth: [] }],
});
};

View file

@ -0,0 +1,15 @@
import { authCookieSchema } from '$lib/server/api/common/openapi/schemas';
import { z } from '@hono/zod-openapi';
export type ZodSchema = z.ZodUnion<never> | z.AnyZodObject | z.ZodArray<z.AnyZodObject>;
type ZodString = z.ZodString;
export function createAuthCookieSchema() {
return createCookieSchema(authCookieSchema);
}
export function createCookieSchema<T extends ZodSchema>(schema: ZodString) {
return z.object({
cookie: schema,
});
}

View file

@ -0,0 +1,7 @@
import { z } from '@hono/zod-openapi';
const cuidParamsSchema = z.object({
cuid: z.string().cuid2(),
});
export default cuidParamsSchema;

View file

@ -0,0 +1,3 @@
import { z } from '@hono/zod-openapi';
export const authCookieSchema = z.string().regex(/^session=\w+$/);

View file

@ -1,14 +1,14 @@
export interface Config {
isProduction: boolean
domain: string
api: ApiConfig
isProduction: boolean;
domain: string;
api: ApiConfig;
// storage: StorageConfig
redis: RedisConfig
postgres: PostgresConfig
redis: RedisConfig;
postgres: PostgresConfig;
}
interface ApiConfig {
origin: string
origin: string;
}
// interface StorageConfig {
@ -19,15 +19,17 @@ interface ApiConfig {
// }
interface RedisConfig {
url: string
url: string;
}
interface PostgresConfig {
user: string
password: string
host: string
port: number
database: string
ssl: boolean
max: number | undefined
user: string;
password: string;
host: string;
port: number;
database: string;
ssl: boolean;
max: number | undefined;
migrating: boolean;
seeding: boolean;
}

View file

@ -1,15 +1,16 @@
import type { Sessions } from '$lib/server/api/databases/postgres/tables';
import type { Hono } from 'hono';
import type { PinoLogger } from 'hono-pino';
import type { Promisify, RateLimitInfo } from 'hono-rate-limiter';
import type { Session, User } from 'lucia';
import type { User } from 'lucia';
// export type AppOpenAPI = OpenAPIHono<AppBindings>;
export type AppOpenAPI = Hono;
export type AppOpenAPI = Hono<AppBindings>;
export type AppBindings = {
Variables: {
logger: PinoLogger;
session: Session | null;
session: Sessions | null;
user: User | null;
rateLimit: RateLimitInfo;
rateLimitStore: {
@ -22,7 +23,7 @@ export type AppBindings = {
export type HonoTypes = {
Variables: {
logger: PinoLogger;
session: Session | null;
session: Sessions | null;
user: User | null;
rateLimit: RateLimitInfo;
rateLimitStore: {

View file

@ -0,0 +1,63 @@
import { config } from '$lib/server/api/common/config';
import env from '$lib/server/api/common/env';
import type { Context } from 'hono';
import { setCookie } from 'hono/cookie';
import type { CookieOptions } from 'hono/utils/cookie';
import { TimeSpan } from 'oslo';
export const cookieMaxAge = 60 * 60 * 24 * 30;
export const cookieExpiresMilliseconds = new TimeSpan(2, 'w').milliseconds();
export const cookieExpiresAt = new Date(Date.now() + cookieExpiresMilliseconds);
export const halfCookieExpiresMilliseconds = cookieExpiresMilliseconds / 2;
export const halfCookieExpiresAt = new Date(Date.now() + halfCookieExpiresMilliseconds);
export const cookieName = 'session';
export type SessionCookie = {
name: string;
value: string;
attributes: CookieOptions;
};
export function createSessionTokenCookie(token: string, expiresAt: Date): SessionCookie {
return {
name: cookieName,
value: token,
attributes: {
path: '/',
maxAge: cookieMaxAge,
domain: env.DOMAIN,
sameSite: 'lax',
secure: config.isProduction,
httpOnly: true,
expires: expiresAt,
},
};
}
export function createBlankSessionTokenCookie(): SessionCookie {
return {
name: cookieName,
value: '',
attributes: {
path: '/',
maxAge: 0,
domain: env.DOMAIN,
sameSite: 'lax',
secure: config.isProduction,
httpOnly: true,
expires: new Date(0),
},
};
}
export function setSessionCookie(c: Context, sessionCookie: SessionCookie) {
setCookie(c, sessionCookie.name, sessionCookie.value, {
path: sessionCookie.attributes.path,
maxAge: sessionCookie.attributes?.maxAge,
domain: sessionCookie.attributes.domain,
sameSite: sessionCookie.attributes.sameSite as undefined,
secure: sessionCookie.attributes.secure,
httpOnly: sessionCookie.attributes.httpOnly,
expires: sessionCookie.attributes.expires,
});
}

View file

@ -1,17 +1,17 @@
import { timestamp } from 'drizzle-orm/pg-core'
import { customType } from 'drizzle-orm/pg-core'
import { timestamp } from 'drizzle-orm/pg-core';
import { customType } from 'drizzle-orm/pg-core';
export const citext = customType<{ data: string }>({
dataType() {
return 'citext'
return 'citext';
},
})
});
export const cuid2 = customType<{ data: string }>({
dataType() {
return 'text'
return 'text';
},
})
});
export const timestamps = {
createdAt: timestamp('created_at', {
@ -25,5 +25,6 @@ export const timestamps = {
withTimezone: true,
})
.notNull()
.defaultNow(),
}
.defaultNow()
.$onUpdate(() => new Date()),
};

View file

@ -1,6 +1,4 @@
// import type { AppOpenAPI } from '$lib/server/api/common/types/hono';
import { apiReference } from '@scalar/hono-api-reference';
import { Hono } from 'hono';
import type { AppOpenAPI } from '$lib/server/api/common/types/hono';
// import { createOpenApiDocument } from 'hono-zod-openapi';
@ -40,6 +38,19 @@ export default function configureOpenAPI(app: AppOpenAPI) {
description: 'Bored Game API',
version: packageJSON.version,
},
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
},
cookieAuth: {
type: 'apiKey',
name: 'session',
in: 'cookie',
}
},
},
});
app.get(

View file

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

View file

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

View file

@ -1,6 +1,6 @@
import { StatusCodes } from '$lib/constants/status-codes';
import { Controller } from '$lib/server/api/common/types/controller';
import { iam, updateProfile } from '$lib/server/api/controllers/iam.routes';
import { createBlankSessionTokenCookie, setSessionCookie } from '$lib/server/api/common/utils/cookies';
import { changePasswordDto } from '$lib/server/api/dtos/change-password.dto';
import { updateEmailDto } from '$lib/server/api/dtos/update-email.dto';
import { updateProfileDto } from '$lib/server/api/dtos/update-profile.dto';
@ -8,19 +8,20 @@ import { verifyPasswordDto } from '$lib/server/api/dtos/verify-password.dto';
import { limiter } from '$lib/server/api/middleware/rate-limiter.middleware';
import { IamService } from '$lib/server/api/services/iam.service';
import { LoginRequestsService } from '$lib/server/api/services/loginrequest.service';
import { LuciaService } from '$lib/server/api/services/lucia.service';
import { SessionsService } from '$lib/server/api/services/sessions.service';
import { zValidator } from '@hono/zod-validator';
import { openApi } from 'hono-zod-openapi';
import { setCookie } from 'hono/cookie';
import { inject, injectable } from 'tsyringe';
import { requireAuth } from '../middleware/require-auth.middleware';
import { iam, logout, updateEmail, updatePassword, updateProfile, verifyPassword } from './iam.routes';
@injectable()
export class IamController extends Controller {
constructor(
@inject(IamService) private readonly iamService: IamService,
@inject(LoginRequestsService) private readonly loginRequestService: LoginRequestsService,
@inject(LuciaService) private luciaService: LuciaService,
@inject(SessionsService) private sessionsService: SessionsService,
) {
super();
}
@ -42,69 +43,74 @@ export class IamController extends Controller {
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('Username already in use', StatusCodes.UNPROCESSABLE_ENTITY);
}
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);
})
.put('/update/password', requireAuth, zValidator('json', changePasswordDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
const user = c.var.user;
const { password, confirm_password } = c.req.valid('json');
if (password !== confirm_password) {
return c.json('Passwords do not match', StatusCodes.BAD_REQUEST);
}
try {
await this.iamService.updatePassword(user.id, { password, confirm_password });
await this.luciaService.lucia.invalidateUserSessions(user.id);
await this.loginRequestService.createUserSession(user.id, c.req, undefined);
const sessionCookie = this.luciaService.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' });
} catch (error) {
console.error('Error updating password', error);
return c.json('Error updating password', StatusCodes.BAD_REQUEST);
}
})
.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) => {
.post(
'/verify/password',
requireAuth,
zValidator('json', verifyPasswordDto),
openApi(verifyPassword),
limiter({ limit: 10, minutes: 60 }),
async (c) => {
const user = c.var.user;
const { password } = c.req.valid('json');
const passwordVerified = await this.iamService.verifyPassword(user.id, { password });
if (!passwordVerified) {
console.log('Incorrect password');
return c.json('Incorrect password', StatusCodes.FORBIDDEN);
}
return c.json({}, StatusCodes.OK);
},
)
.put(
'/update/password',
requireAuth,
openApi(updatePassword),
zValidator('json', changePasswordDto),
limiter({ limit: 10, minutes: 60 }),
async (c) => {
const user = c.var.user;
const { password, confirm_password } = c.req.valid('json');
if (password !== confirm_password) {
return c.json('Passwords do not match', StatusCodes.UNPROCESSABLE_ENTITY);
}
try {
await this.iamService.updatePassword(user.id, { password, confirm_password });
await this.sessionsService.invalidateSession(user.id);
await this.loginRequestService.createUserSession(user.id, c.req, undefined);
const sessionCookie = createBlankSessionTokenCookie();
setSessionCookie(c, sessionCookie);
return c.json({ status: 'success' });
} catch (error) {
console.error('Error updating password', error);
return c.json('Error updating password', StatusCodes.INTERNAL_SERVER_ERROR);
}
},
)
.post(
'/update/email',
requireAuth,
openApi(updateEmail),
zValidator('json', updateEmailDto),
limiter({ limit: 10, minutes: 60 }),
async (c) => {
const user = c.var.user;
const { email } = c.req.valid('json');
const updatedUser = await this.iamService.updateEmail(user.id, { email });
if (!updatedUser) {
return c.json('Cannot change email address', StatusCodes.FORBIDDEN);
}
return c.json({ user: updatedUser }, StatusCodes.OK);
},
)
.post('/logout', requireAuth, openApi(logout), async (c) => {
const sessionId = c.var.session.id;
await this.iamService.logout(sessionId);
const sessionCookie = this.luciaService.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,
});
const sessionCookie = createBlankSessionTokenCookie();
setSessionCookie(c, sessionCookie);
return c.json({ status: 'success' });
});
}

View file

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

View file

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

View file

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

View file

@ -1,14 +1,14 @@
import 'reflect-metadata'
import { StatusCodes } from '$lib/constants/status-codes'
import { Controller } from '$lib/server/api/common/types/controller'
import { verifyTotpDto } from '$lib/server/api/dtos/verify-totp.dto'
import { RecoveryCodesService } from '$lib/server/api/services/recovery-codes.service'
import { TotpService } from '$lib/server/api/services/totp.service'
import { UsersService } from '$lib/server/api/services/users.service'
import { zValidator } from '@hono/zod-validator'
import { inject, injectable } from 'tsyringe'
import { CredentialsType } from '../databases/tables'
import { requireAuth } from '../middleware/require-auth.middleware'
import 'reflect-metadata';
import { StatusCodes } from '$lib/constants/status-codes';
import { Controller } from '$lib/server/api/common/types/controller';
import { verifyTotpDto } from '$lib/server/api/dtos/verify-totp.dto';
import { RecoveryCodesService } from '$lib/server/api/services/recovery-codes.service';
import { TotpService } from '$lib/server/api/services/totp.service';
import { UsersService } from '$lib/server/api/services/users.service';
import { zValidator } from '@hono/zod-validator';
import { inject, injectable } from 'tsyringe';
import { CredentialsType } from '../databases/postgres/tables';
import { requireAuth } from '../middleware/require-auth.middleware';
@injectable()
export class MfaController extends Controller {
@ -17,59 +17,59 @@ export class MfaController extends Controller {
@inject(TotpService) private readonly totpService: TotpService,
@inject(UsersService) private readonly usersService: UsersService,
) {
super()
super();
}
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 })
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 })
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
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)
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)
console.error(e);
return c.status(StatusCodes.INTERNAL_SERVER_ERROR);
}
})
.get('/totp/recoveryCodes', requireAuth, async (c) => {
const user = c.var.user
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)
const existingCodes = await this.recoveryCodesService.findAllRecoveryCodesByUserId(user.id);
if (existingCodes && existingCodes.length > 0) {
console.log('Recovery Codes found', existingCodes)
return c.json({ recoveryCodes: existingCodes })
console.log('Recovery Codes found', existingCodes);
return c.json({ recoveryCodes: existingCodes });
}
const recoveryCodes = await this.recoveryCodesService.createRecoveryCodes(user.id)
return c.json({ recoveryCodes })
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)
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)
await this.usersService.updateUser(user.id, { mfa_enabled: true });
return c.json({}, StatusCodes.OK);
}
return c.json('Invalid code', StatusCodes.BAD_REQUEST)
return c.json('Invalid code', StatusCodes.BAD_REQUEST);
} catch (e) {
console.error(e)
return c.status(StatusCodes.INTERNAL_SERVER_ERROR)
console.error(e);
return c.status(StatusCodes.INTERNAL_SERVER_ERROR);
}
})
});
}
}

View file

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

View file

@ -1,43 +1,43 @@
import 'reflect-metadata'
import { Controller } from '$lib/server/api/common/types/controller'
import { signupUsernameEmailDto } from '$lib/server/api/dtos/signup-username-email.dto'
import { limiter } from '$lib/server/api/middleware/rate-limiter.middleware'
import { LoginRequestsService } from '$lib/server/api/services/loginrequest.service'
import { LuciaService } from '$lib/server/api/services/lucia.service'
import { UsersService } from '$lib/server/api/services/users.service'
import { zValidator } from '@hono/zod-validator'
import { setCookie } from 'hono/cookie'
import { TimeSpan } from 'oslo'
import { inject, injectable } from 'tsyringe'
import 'reflect-metadata';
import { Controller } from '$lib/server/api/common/types/controller';
import { signupUsernameEmailDto } from '$lib/server/api/dtos/signup-username-email.dto';
import { limiter } from '$lib/server/api/middleware/rate-limiter.middleware';
import { LoginRequestsService } from '$lib/server/api/services/loginrequest.service';
import { SessionsService } from '$lib/server/api/services/sessions.service';
import { UsersService } from '$lib/server/api/services/users.service';
import { zValidator } from '@hono/zod-validator';
import { setCookie } from 'hono/cookie';
import { TimeSpan } from 'oslo';
import { inject, injectable } from 'tsyringe';
@injectable()
export class SignupController extends Controller {
constructor(
@inject(UsersService) private readonly usersService: UsersService,
@inject(LoginRequestsService) private readonly loginRequestService: LoginRequestsService,
@inject(LuciaService) private luciaService: LuciaService,
@inject(SessionsService) private luciaService: SessionsService,
) {
super()
super();
}
routes() {
return this.controller.post('/', zValidator('json', signupUsernameEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
const { firstName, lastName, email, username, password, confirm_password } = await c.req.valid('json')
const existingUser = await this.usersService.findOneByUsername(username)
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)
return c.body('User already exists', 400);
}
const user = await this.usersService.create({ firstName, lastName, email, username, password, confirm_password })
const user = await this.usersService.create({ firstName, lastName, email, username, password, confirm_password });
if (!user) {
return c.body('Failed to create user', 500)
return c.body('Failed to create user', 500);
}
const session = await this.loginRequestService.createUserSession(user.id, c.req, undefined)
const sessionCookie = this.luciaService.lucia.createSessionCookie(session.id)
console.log('set cookie', sessionCookie)
const session = await this.loginRequestService.createUserSession(user.id, c.req, undefined);
const sessionCookie = this.luciaService.lucia.createSessionCookie(session.id);
console.log('set cookie', sessionCookie);
setCookie(c, sessionCookie.name, sessionCookie.value, {
path: sessionCookie.attributes.path,
maxAge:
@ -49,8 +49,8 @@ export class SignupController extends Controller {
secure: sessionCookie.attributes.secure,
httpOnly: sessionCookie.attributes.httpOnly,
expires: sessionCookie.attributes.expires,
})
return c.json({ message: 'ok' })
})
});
return c.json({ message: 'ok' });
});
}
}

View file

@ -1,30 +0,0 @@
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 '../common/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: false, // env.NODE_ENV === 'development' ? false : 'require',
max: 1,
})
const db = drizzle(connection)
try {
if (!config.out) {
console.error('No migrations folder specified in drizzle.config.ts')
process.exit()
}
await migrate(db, { migrationsFolder: config.out })
console.log('Migrations complete')
} catch (e) {
console.error(e)
}
process.exit()

View file

@ -1,20 +0,0 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1725489682980,
"tag": "0000_volatile_warhawk",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1726877846811,
"tag": "0001_pink_the_enforcers",
"breakpoints": true
}
]
}

View file

@ -0,0 +1,20 @@
import 'dotenv/config';
import { migrate } from 'drizzle-orm/node-postgres/migrator';
import config from '../../../../../../drizzle.config';
import env from '../../common/env';
import { DrizzleService } from '../../services/drizzle.service';
const drizzleService = new DrizzleService();
if (!config.out) {
console.error('No migrations folder specified in drizzle.config.ts');
process.exit();
}
if (!env.DB_MIGRATING) {
throw new Error('You must set DB_MIGRATING to "true" when running migrations.');
}
await migrate(drizzleService.db, { migrationsFolder: config.out });
console.log('Migrations complete');
await drizzleService.dispose();
process.exit();

View file

@ -0,0 +1,7 @@
ALTER TABLE "sessions" DROP CONSTRAINT "sessions_user_id_users_id_fk";
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_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 $$;

View file

@ -0,0 +1,2 @@
ALTER TABLE "two_factor" DROP CONSTRAINT "two_factor_user_id_unique";--> statement-breakpoint
ALTER TABLE "two_factor" ADD CONSTRAINT "two_factor_userId_unique" UNIQUE("user_id");

View file

@ -0,0 +1,2 @@
ALTER TABLE "two_factor" DROP CONSTRAINT "two_factor_userId_unique";--> statement-breakpoint
ALTER TABLE "two_factor" ADD CONSTRAINT "two_factor_user_id_unique" UNIQUE("user_id");

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,41 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1725489682980,
"tag": "0000_volatile_warhawk",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1726877846811,
"tag": "0001_pink_the_enforcers",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1729273181237,
"tag": "0002_mysterious_justice",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1730914649946,
"tag": "0003_far_siren",
"breakpoints": true
},
{
"idx": 4,
"version": "7",
"when": 1730914773500,
"tag": "0004_lush_virginia_dare",
"breakpoints": true
}
]
}

View file

@ -1,6 +1,6 @@
import { collections } from '$lib/server/api/databases/tables'
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
import type { z } from 'zod'
import { collections } from '$lib/server/api/databases/postgres/tables';
import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
import type { z } from 'zod';
export const InsertCollectionSchema = createInsertSchema(collections, {
name: (schema) =>
@ -10,15 +10,15 @@ export const InsertCollectionSchema = createInsertSchema(collections, {
cuid: true,
createdAt: true,
updatedAt: true,
})
});
export type InsertCollectionSchema = z.infer<typeof InsertCollectionSchema>
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>
export type SelectUserSchema = z.infer<typeof SelectCollectionSchema>;

View file

@ -1,6 +1,6 @@
import { usersTable } from '$lib/server/api/databases/tables'
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
import type { z } from 'zod'
import { usersTable } from '$lib/server/api/databases/postgres/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(),
@ -15,10 +15,10 @@ export const InsertUserSchema = createInsertSchema(usersTable, {
cuid: true,
createdAt: true,
updatedAt: true,
})
});
export type InsertUserSchema = z.infer<typeof InsertUserSchema>
export type InsertUserSchema = z.infer<typeof InsertUserSchema>;
export const SelectUserSchema = createSelectSchema(usersTable)
export const SelectUserSchema = createSelectSchema(usersTable);
export type SelectUserSchema = z.infer<typeof SelectUserSchema>
export type SelectUserSchema = z.infer<typeof SelectUserSchema>;

View file

@ -1,19 +1,18 @@
import 'reflect-metadata'
import { type Table, getTableName, sql } from 'drizzle-orm'
import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
import env from '../common/env'
import { DrizzleService } from '../services/drizzle.service'
import * as seeds from './seeds'
import * as schema from './tables'
import { type Table, getTableName, sql } from 'drizzle-orm';
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
import env from '../../common/env';
import { DrizzleService } from '../../services/drizzle.service';
import * as seeds from './seeds';
import * as schema from './tables';
const drizzleService = new DrizzleService()
const drizzleService = new DrizzleService();
if (!env.DB_SEEDING) {
throw new Error('You must set DB_SEEDING to "true" when running seeds')
throw new Error('You must set DB_SEEDING to "true" when running seeds');
}
async function resetTable(db: NodePgDatabase<typeof schema>, table: Table) {
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 [
@ -45,11 +44,11 @@ for (const table of [
schema.wishlistsTable,
]) {
// await db.delete(table); // clear tables without truncating / resetting ids
await resetTable(drizzleService.db, table)
await resetTable(drizzleService.db, table);
}
await seeds.roles(drizzleService.db)
await seeds.users(drizzleService.db)
await seeds.roles(drizzleService.db);
await seeds.users(drizzleService.db);
await drizzleService.dispose()
process.exit()
await drizzleService.dispose();
process.exit();

View file

@ -0,0 +1,11 @@
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
import * as schema from '../tables';
import roles from './data/roles.json';
export default async function seed(db: NodePgDatabase<typeof schema>) {
console.log('Creating rolesTable ...');
for (const role of roles) {
await db.insert(schema.rolesTable).values(role).onConflictDoNothing();
}
console.log('Roles created.');
}

View file

@ -1,6 +1,6 @@
import { eq } from 'drizzle-orm';
import type { db } from '../../packages/drizzle';
import { HashingService } from '../../services/hashing.service';
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { HashingService } from '../../../services/hashing.service';
import * as schema from '../tables';
import users from './data/users.json';
@ -9,7 +9,7 @@ type JsonRole = {
primary: boolean;
};
export default async function seed(db: db) {
export default async function seed(db: NodePgDatabase<typeof schema>) {
const hashingService = new HashingService();
const adminRole = await db.select().from(schema.rolesTable).where(eq(schema.rolesTable.name, 'admin'));
const userRole = await db.select().from(schema.rolesTable).where(eq(schema.rolesTable.name, 'user'));

View file

@ -1,23 +1,23 @@
import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table'
import { categoriesToExternalIdsTable } from './categoriesToExternalIds.table'
import { categories_to_games_table } from './categoriesToGames.table'
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { timestamps } from '../../../common/utils/table';
import { categoriesToExternalIdsTable } from './categoriesToExternalIds.table';
import { categories_to_games_table } from './categoriesToGames.table';
export const categoriesTable = pgTable('categories', {
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
id: uuid().primaryKey().defaultRandom(),
cuid: text()
.unique()
.$defaultFn(() => cuid2()),
name: text('name'),
slug: text('slug'),
name: text(),
slug: text(),
...timestamps,
})
});
export type Categories = InferSelectModel<typeof categoriesTable>
export type Categories = InferSelectModel<typeof categoriesTable>;
export const categories_relations = relations(categoriesTable, ({ many }) => ({
categories_to_games: many(categories_to_games_table),
categoriesToExternalIds: many(categoriesToExternalIdsTable),
}))
}));

View file

@ -1,15 +1,15 @@
import { relations } from 'drizzle-orm'
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core'
import { categoriesTable } from './categories.table'
import { externalIdsTable } from './externalIds.table'
import { relations } from 'drizzle-orm';
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core';
import { categoriesTable } from './categories.table';
import { externalIdsTable } from './externalIds.table';
export const categoriesToExternalIdsTable = pgTable(
'categories_to_external_ids',
{
categoryId: uuid('category_id')
categoryId: uuid()
.notNull()
.references(() => categoriesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
externalId: uuid('external_id')
externalId: uuid()
.notNull()
.references(() => externalIdsTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
},
@ -18,9 +18,9 @@ export const categoriesToExternalIdsTable = pgTable(
categoriesToExternalIdsPkey: primaryKey({
columns: [table.categoryId, table.externalId],
}),
}
};
},
)
);
export const categoriesToExternalIdsRelations = relations(categoriesToExternalIdsTable, ({ one }) => ({
category: one(categoriesTable, {
@ -31,4 +31,4 @@ export const categoriesToExternalIdsRelations = relations(categoriesToExternalId
fields: [categoriesToExternalIdsTable.externalId],
references: [externalIdsTable.id],
}),
}))
}));

View file

@ -1,15 +1,15 @@
import { relations } from 'drizzle-orm'
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core'
import { gamesTable } from '././games.table'
import { categoriesTable } from './categories.table'
import { relations } from 'drizzle-orm';
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core';
import { categoriesTable } from './categories.table';
import { gamesTable } from './games.table';
export const categories_to_games_table = pgTable(
'categories_to_games',
{
category_id: uuid('category_id')
category_id: uuid()
.notNull()
.references(() => categoriesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
game_id: uuid('game_id')
game_id: uuid()
.notNull()
.references(() => gamesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
},
@ -18,9 +18,9 @@ export const categories_to_games_table = pgTable(
categoriesToGamesPkey: primaryKey({
columns: [table.category_id, table.game_id],
}),
}
};
},
)
);
export const categories_to_games_relations = relations(categories_to_games_table, ({ one }) => ({
category: one(categoriesTable, {
@ -31,4 +31,4 @@ export const categories_to_games_relations = relations(categories_to_games_table
fields: [categories_to_games_table.game_id],
references: [gamesTable.id],
}),
}))
}));

View file

@ -1,26 +1,26 @@
import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations } from 'drizzle-orm'
import { integer, pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table'
import { gamesTable } from '././games.table'
import { collections } from './collections.table'
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import { integer, pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { timestamps } from '../../../common/utils/table';
import { collections } from './collections.table';
import { gamesTable } from './games.table';
export const collection_items = pgTable('collection_items', {
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
id: uuid().primaryKey().defaultRandom(),
cuid: text()
.unique()
.$defaultFn(() => cuid2()),
collection_id: uuid('collection_id')
collection_id: uuid()
.notNull()
.references(() => collections.id, { onDelete: 'cascade' }),
game_id: uuid('game_id')
game_id: uuid()
.notNull()
.references(() => gamesTable.id, { onDelete: 'cascade' }),
times_played: integer('times_played').default(0),
times_played: integer().default(0),
...timestamps,
})
});
export type CollectionItemsTable = InferSelectModel<typeof collection_items>
export type CollectionItemsTable = InferSelectModel<typeof collection_items>;
export const collection_item_relations = relations(collection_items, ({ one }) => ({
collection: one(collections, {
@ -31,4 +31,4 @@ export const collection_item_relations = relations(collection_items, ({ one }) =
fields: [collection_items.game_id],
references: [gamesTable.id],
}),
}))
}));

View file

@ -0,0 +1,31 @@
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { createSelectSchema } from 'drizzle-zod';
import { timestamps } from '../../../common/utils/table';
import { collection_items } from './collectionItems.table';
import { usersTable } from './users.table';
export const collections = pgTable('collections', {
id: uuid().primaryKey().defaultRandom(),
cuid: text()
.unique()
.$defaultFn(() => cuid2()),
user_id: uuid()
.notNull()
.references(() => usersTable.id, { onDelete: 'cascade' }),
name: text().notNull().default('My Collection'),
...timestamps,
});
export const collection_relations = relations(collections, ({ one, many }) => ({
user: one(usersTable, {
fields: [collections.user_id],
references: [usersTable.id],
}),
collection_items: many(collection_items),
}));
export const selectCollectionSchema = createSelectSchema(collections);
export type Collections = InferSelectModel<typeof collections>;

View file

@ -0,0 +1,23 @@
import type { InferSelectModel } from 'drizzle-orm';
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { timestamps } from '../../../common/utils/table';
import { usersTable } from './users.table';
export enum CredentialsType {
SECRET = 'secret',
PASSWORD = 'password',
TOTP = 'totp',
HOTP = 'hotp',
}
export const credentialsTable = pgTable('credentials', {
id: uuid().primaryKey().defaultRandom(),
user_id: uuid()
.notNull()
.references(() => usersTable.id, { onDelete: 'cascade' }),
type: text().notNull().default(CredentialsType.PASSWORD),
secret_data: text().notNull(),
...timestamps,
});
export type Credentials = InferSelectModel<typeof credentialsTable>;

View file

@ -1,24 +1,24 @@
import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table'
import { gamesTable } from '././games.table'
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { timestamps } from '../../../common/utils/table';
import { gamesTable } from './games.table';
export const expansionsTable = pgTable('expansions', {
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
id: uuid().primaryKey().defaultRandom(),
cuid: text()
.unique()
.$defaultFn(() => cuid2()),
base_game_id: uuid('base_game_id')
base_game_id: uuid()
.notNull()
.references(() => gamesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
game_id: uuid('game_id')
game_id: uuid()
.notNull()
.references(() => gamesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
...timestamps,
})
});
export type Expansions = InferSelectModel<typeof expansionsTable>
export type Expansions = InferSelectModel<typeof expansionsTable>;
export const expansion_relations = relations(expansionsTable, ({ one }) => ({
baseGame: one(gamesTable, {
@ -29,4 +29,4 @@ export const expansion_relations = relations(expansionsTable, ({ one }) => ({
fields: [expansionsTable.game_id],
references: [gamesTable.id],
}),
}))
}));

View file

@ -0,0 +1,32 @@
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { timestamps } from '../../../common/utils/table';
import { gamesTable } from './games.table';
export const expansions = pgTable('expansions', {
id: uuid().primaryKey().defaultRandom(),
cuid: text()
.unique()
.$defaultFn(() => cuid2()),
base_game_id: uuid()
.notNull()
.references(() => gamesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
game_id: uuid()
.notNull()
.references(() => gamesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
...timestamps,
});
export type Expansions = InferSelectModel<typeof expansions>;
export const expansion_relations = relations(expansions, ({ one }) => ({
baseGame: one(gamesTable, {
fields: [expansions.base_game_id],
references: [gamesTable.id],
}),
game: one(gamesTable, {
fields: [expansions.game_id],
references: [gamesTable.id],
}),
}));

View file

@ -0,0 +1,16 @@
import { createId as cuid2 } from '@paralleldrive/cuid2';
import type { InferSelectModel } from 'drizzle-orm';
import { pgEnum, pgTable, text, uuid } from 'drizzle-orm/pg-core';
export const externalIdType = pgEnum('external_id_type', ['game', 'category', 'mechanic', 'publisher', 'designer', 'artist']);
export const externalIdsTable = pgTable('external_ids', {
id: uuid().primaryKey().defaultRandom(),
cuid: text()
.unique()
.$defaultFn(() => cuid2()),
type: externalIdType(),
externalId: text().notNull(),
});
export type ExternalIds = InferSelectModel<typeof externalIdsTable>;

View file

@ -0,0 +1,17 @@
import type { InferSelectModel } from 'drizzle-orm';
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { timestamps } from '../../../common/utils/table';
import { usersTable } from './users.table';
export const federatedIdentityTable = pgTable('federated_identity', {
id: uuid().primaryKey().defaultRandom(),
user_id: uuid()
.notNull()
.references(() => usersTable.id, { onDelete: 'cascade' }),
identity_provider: text().notNull(),
federated_user_id: text().notNull(),
federated_username: text().notNull(),
...timestamps,
});
export type FederatedIdentity = InferSelectModel<typeof federatedIdentityTable>;

View file

@ -0,0 +1,51 @@
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations, sql } from 'drizzle-orm';
import { index, integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
import { timestamps } from '../../../common/utils/table';
import { categories_to_games_table } from './categoriesToGames.table';
import { gamesToExternalIdsTable } from './gamesToExternalIds.table';
import { mechanics_to_games } from './mechanicsToGames.table';
import { publishers_to_games } from './publishersToGames.table';
export const gamesTable = pgTable(
'games',
{
id: uuid().primaryKey().defaultRandom(),
cuid: text()
.unique()
.$defaultFn(() => cuid2()),
name: text().notNull(),
slug: text().notNull(),
description: text(),
year_published: integer(),
min_players: integer(),
max_players: integer(),
playtime: integer(),
min_playtime: integer(),
max_playtime: integer(),
min_age: integer(),
image_url: text(),
thumb_url: text(),
url: text(),
last_sync_at: timestamp(),
...timestamps,
},
(table) => ({
searchIndex: index('search_index').using(
'gin',
sql`(
setweight(to_tsvector('english', ${table.name}), 'A') ||
setweight(to_tsvector('english', ${table.slug}), 'B')
)`,
),
}),
);
export const gameRelations = relations(gamesTable, ({ many }) => ({
categories_to_games: many(categories_to_games_table),
mechanics_to_games: many(mechanics_to_games),
publishers_to_games: many(publishers_to_games),
gamesToExternalIds: many(gamesToExternalIdsTable),
}));
export type Games = InferSelectModel<typeof gamesTable>;

View file

@ -1,15 +1,15 @@
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core'
import { gamesTable } from '././games.table'
import { externalIdsTable } from './externalIds.table'
import { relations } from 'drizzle-orm'
import { relations } from 'drizzle-orm';
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core';
import { externalIdsTable } from './externalIds.table';
import { gamesTable } from './games.table';
export const gamesToExternalIdsTable = pgTable(
'games_to_external_ids',
{
gameId: uuid('game_id')
gameId: uuid()
.notNull()
.references(() => gamesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
externalId: uuid('external_id')
externalId: uuid()
.notNull()
.references(() => externalIdsTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
},
@ -18,9 +18,9 @@ export const gamesToExternalIdsTable = pgTable(
gamesToExternalIdsPkey: primaryKey({
columns: [table.gameId, table.externalId],
}),
}
};
},
)
);
export const gamesToExternalIdsRelations = relations(gamesToExternalIdsTable, ({ one }) => ({
game: one(gamesTable, {
@ -31,4 +31,4 @@ export const gamesToExternalIdsRelations = relations(gamesToExternalIdsTable, ({
fields: [gamesToExternalIdsTable.externalId],
references: [externalIdsTable.id],
}),
}))
}));

View file

@ -0,0 +1,23 @@
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { timestamps } from '../../../common/utils/table';
import { mechanicsToExternalIdsTable } from './mechanicsToExternalIds.table';
import { mechanics_to_games } from './mechanicsToGames.table';
export const mechanicsTable = pgTable('mechanics', {
id: uuid().primaryKey().defaultRandom(),
cuid: text()
.unique()
.$defaultFn(() => cuid2()),
name: text(),
slug: text(),
...timestamps,
});
export type Mechanics = InferSelectModel<typeof mechanicsTable>;
export const mechanics_relations = relations(mechanicsTable, ({ many }) => ({
mechanics_to_games: many(mechanics_to_games),
mechanicsToExternalIds: many(mechanicsToExternalIdsTable),
}));

View file

@ -0,0 +1,23 @@
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { timestamps } from '../../../common/utils/table';
import { mechanicsToExternalIdsTable } from './mechanicsToExternalIds.table';
import { mechanics_to_games } from './mechanicsToGames.table';
export const mechanics = pgTable('mechanics', {
id: uuid().primaryKey().defaultRandom(),
cuid: text()
.unique()
.$defaultFn(() => cuid2()),
name: text(),
slug: text(),
...timestamps,
});
export type Mechanics = InferSelectModel<typeof mechanics>;
export const mechanics_relations = relations(mechanics, ({ many }) => ({
mechanics_to_games: many(mechanics_to_games),
mechanicsToExternalIds: many(mechanicsToExternalIdsTable),
}));

View file

@ -1,15 +1,15 @@
import { relations } from 'drizzle-orm'
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core'
import { gamesTable } from '././games.table'
import { mechanicsTable } from './mechanics.table'
import { relations } from 'drizzle-orm';
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core';
import { gamesTable } from './games.table';
import { mechanicsTable } from './mechanics.table';
export const mechanics_to_games = pgTable(
'mechanics_to_games',
{
mechanic_id: uuid('mechanic_id')
mechanic_id: uuid()
.notNull()
.references(() => mechanicsTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
game_id: uuid('game_id')
game_id: uuid()
.notNull()
.references(() => gamesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
},
@ -18,9 +18,9 @@ export const mechanics_to_games = pgTable(
mechanicsToGamesPkey: primaryKey({
columns: [table.mechanic_id, table.game_id],
}),
}
};
},
)
);
export const mechanics_to_games_relations = relations(mechanics_to_games, ({ one }) => ({
mechanic: one(mechanicsTable, {
@ -31,4 +31,4 @@ export const mechanics_to_games_relations = relations(mechanics_to_games, ({ one
fields: [mechanics_to_games.game_id],
references: [gamesTable.id],
}),
}))
}));

View file

@ -1,25 +1,25 @@
import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations } from 'drizzle-orm'
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table'
import { usersTable } from './users.table'
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
import { timestamps } from '../../../common/utils/table';
import { usersTable } from './users.table';
export const password_reset_tokens = pgTable('password_reset_tokens', {
id: text('id')
id: text()
.primaryKey()
.$defaultFn(() => cuid2()),
user_id: uuid('user_id')
user_id: uuid()
.notNull()
.references(() => usersTable.id, { onDelete: 'cascade' }),
expires_at: timestamp('expires_at'),
expires_at: timestamp(),
...timestamps,
})
});
export type PasswordResetTokensTable = InferSelectModel<typeof password_reset_tokens>
export type PasswordResetTokensTable = InferSelectModel<typeof password_reset_tokens>;
export const password_reset_token_relations = relations(password_reset_tokens, ({ one }) => ({
user: one(usersTable, {
fields: [password_reset_tokens.user_id],
references: [usersTable.id],
}),
}))
}));

View file

@ -1,23 +1,23 @@
import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table'
import { publishersToExternalIdsTable } from './publishersToExternalIds.table'
import { publishers_to_games } from './publishersToGames.table'
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { timestamps } from '../../../common/utils/table';
import { publishersToExternalIdsTable } from './publishersToExternalIds.table';
import { publishers_to_games } from './publishersToGames.table';
export const publishersTable = pgTable('publishers', {
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
id: uuid().primaryKey().defaultRandom(),
cuid: text()
.unique()
.$defaultFn(() => cuid2()),
name: text('name'),
slug: text('slug'),
name: text(),
slug: text(),
...timestamps,
})
});
export type Publishers = InferSelectModel<typeof publishersTable>
export type Publishers = InferSelectModel<typeof publishersTable>;
export const publishers_relations = relations(publishersTable, ({ many }) => ({
publishersToGames: many(publishers_to_games),
publishersToExternalIds: many(publishersToExternalIdsTable),
}))
}));

View file

@ -0,0 +1,23 @@
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { timestamps } from '../../../common/utils/table';
import { publishersToExternalIdsTable } from './publishersToExternalIds.table';
import { publishers_to_games } from './publishersToGames.table';
export const publishers = pgTable('publishers', {
id: uuid().primaryKey().defaultRandom(),
cuid: text()
.unique()
.$defaultFn(() => cuid2()),
name: text(),
slug: text(),
...timestamps,
});
export type Publishers = InferSelectModel<typeof publishers>;
export const publishers_relations = relations(publishers, ({ many }) => ({
publishers_to_games: many(publishers_to_games),
publishersToExternalIds: many(publishersToExternalIdsTable),
}));

View file

@ -1,15 +1,15 @@
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core'
import { externalIdsTable } from './externalIds.table'
import { publishersTable } from './publishers.table'
import { relations } from 'drizzle-orm'
import { relations } from 'drizzle-orm';
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core';
import { externalIdsTable } from './externalIds.table';
import { publishersTable } from './publishers.table';
export const publishersToExternalIdsTable = pgTable(
'publishers_to_external_ids',
{
publisherId: uuid('publisher_id')
publisherId: uuid()
.notNull()
.references(() => publishersTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
externalId: uuid('external_id')
externalId: uuid()
.notNull()
.references(() => externalIdsTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
},
@ -18,9 +18,9 @@ export const publishersToExternalIdsTable = pgTable(
publishersToExternalIdsPkey: primaryKey({
columns: [table.publisherId, table.externalId],
}),
}
};
},
)
);
export const publishersToExternalIdsRelations = relations(publishersToExternalIdsTable, ({ one }) => ({
publisher: one(publishersTable, {
@ -31,4 +31,4 @@ export const publishersToExternalIdsRelations = relations(publishersToExternalId
fields: [publishersToExternalIdsTable.externalId],
references: [externalIdsTable.id],
}),
}))
}));

View file

@ -1,15 +1,15 @@
import { relations } from 'drizzle-orm'
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core'
import { gamesTable } from '././games.table'
import { publishersTable } from './publishers.table'
import { relations } from 'drizzle-orm';
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core';
import { gamesTable } from './games.table';
import { publishersTable } from './publishers.table';
export const publishers_to_games = pgTable(
'publishers_to_games',
{
publisher_id: uuid('publisher_id')
publisher_id: uuid()
.notNull()
.references(() => publishersTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
game_id: uuid('game_id')
game_id: uuid()
.notNull()
.references(() => gamesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
},
@ -18,9 +18,9 @@ export const publishers_to_games = pgTable(
publishersToGamesPkey: primaryKey({
columns: [table.publisher_id, table.game_id],
}),
}
};
},
)
);
export const publishers_to_games_relations = relations(publishers_to_games, ({ one }) => ({
publisher: one(publishersTable, {
@ -31,4 +31,4 @@ export const publishers_to_games_relations = relations(publishers_to_games, ({ o
fields: [publishers_to_games.game_id],
references: [gamesTable.id],
}),
}))
}));

View file

@ -0,0 +1,16 @@
import type { InferSelectModel } from 'drizzle-orm';
import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { timestamps } from '../../../common/utils/table';
import { usersTable } from './users.table';
export const recoveryCodesTable = pgTable('recovery_codes', {
id: uuid().primaryKey().defaultRandom(),
userId: uuid()
.notNull()
.references(() => usersTable.id),
code: text().notNull(),
used: boolean().default(false),
...timestamps,
});
export type RecoveryCodesTable = InferSelectModel<typeof recoveryCodesTable>;

View file

@ -0,0 +1,28 @@
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { timestamps } from '../../../common/utils/table';
import { user_roles } from './userRoles.table';
export enum RoleName {
ADMIN = 'admin',
EDITOR = 'editor',
MODERATOR = 'moderator',
USER = 'user',
}
export const rolesTable = pgTable('roles', {
id: uuid().primaryKey().defaultRandom(),
cuid: text()
.unique()
.$defaultFn(() => cuid2())
.notNull(),
name: text().unique().notNull(),
...timestamps,
});
export type Roles = InferSelectModel<typeof rolesTable>;
export const role_relations = relations(rolesTable, ({ many }) => ({
user_roles: many(user_roles),
}));

View file

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

View file

@ -0,0 +1,32 @@
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import { boolean, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
import { timestamps } from '../../../common/utils/table';
import { usersTable } from './users.table';
export const twoFactorTable = pgTable('two_factor', {
id: uuid().primaryKey().defaultRandom(),
cuid: text()
.unique()
.$defaultFn(() => cuid2()),
secret: text().notNull(),
enabled: boolean().notNull().default(false),
initiatedTime: timestamp({
mode: 'date',
withTimezone: true,
}),
userId: uuid()
.notNull()
.references(() => usersTable.id)
.unique('two_factor_user_id_unique'),
...timestamps,
});
export const emailVerificationsRelations = relations(twoFactorTable, ({ one }) => ({
user: one(usersTable, {
fields: [twoFactorTable.userId],
references: [usersTable.id],
}),
}));
export type TwoFactor = InferSelectModel<typeof twoFactorTable>;

View file

@ -1,24 +1,24 @@
import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations } from 'drizzle-orm'
import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table'
import { rolesTable } from './roles.table'
import { usersTable } from './users.table'
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { timestamps } from '../../../common/utils/table';
import { rolesTable } from './roles.table';
import { usersTable } from './users.table';
export const user_roles = pgTable('user_roles', {
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
id: uuid().primaryKey().defaultRandom(),
cuid: text()
.unique()
.$defaultFn(() => cuid2()),
user_id: uuid('user_id')
user_id: uuid()
.notNull()
.references(() => usersTable.id, { onDelete: 'cascade' }),
role_id: uuid('role_id')
role_id: uuid()
.notNull()
.references(() => rolesTable.id, { onDelete: 'cascade' }),
primary: boolean('primary').default(false),
primary: boolean().default(false),
...timestamps,
})
});
export const user_role_relations = relations(user_roles, ({ one }) => ({
role: one(rolesTable, {
@ -29,6 +29,6 @@ export const user_role_relations = relations(user_roles, ({ one }) => ({
fields: [user_roles.user_id],
references: [usersTable.id],
}),
}))
}));
export type UserRolesTable = InferSelectModel<typeof user_roles>
export type UserRolesTable = InferSelectModel<typeof user_roles>;

View file

@ -2,24 +2,24 @@ import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { createSelectSchema } from 'drizzle-zod';
import { timestamps } from '../../common/utils/table';
import { timestamps } from '../../../common/utils/table';
import { user_roles } from './userRoles.table';
export const usersTable = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
id: uuid().primaryKey().defaultRandom(),
cuid: text()
.unique()
.$defaultFn(() => cuid2()),
username: text('username').unique(),
email: text('email').unique(),
first_name: text('first_name'),
last_name: text('last_name'),
verified: boolean('verified').default(false),
receive_email: boolean('receive_email').default(false),
email_verified: boolean('email_verified').default(false),
picture: text('picture'),
mfa_enabled: boolean('mfa_enabled').notNull().default(false),
theme: text('theme').default('system'),
username: text().unique(),
email: text().unique(),
first_name: text(),
last_name: text(),
verified: boolean().default(false),
receive_email: boolean().default(false),
email_verified: boolean().default(false),
picture: text(),
mfa_enabled: boolean().notNull().default(false),
theme: text().default('system'),
...timestamps,
});

View file

@ -1,25 +1,25 @@
import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table'
import { gamesTable } from '././games.table'
import { wishlistsTable } from './wishlists.table'
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { timestamps } from '../../../common/utils/table';
import { gamesTable } from './games.table';
import { wishlistsTable } from './wishlists.table';
export const wishlist_items = pgTable('wishlist_items', {
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
id: uuid().primaryKey().defaultRandom(),
cuid: text()
.unique()
.$defaultFn(() => cuid2()),
wishlist_id: uuid('wishlist_id')
wishlist_id: uuid()
.notNull()
.references(() => wishlistsTable.id, { onDelete: 'cascade' }),
game_id: uuid('game_id')
game_id: uuid()
.notNull()
.references(() => gamesTable.id, { onDelete: 'cascade' }),
...timestamps,
})
});
export type WishlistItemsTable = InferSelectModel<typeof wishlist_items>
export type WishlistItemsTable = InferSelectModel<typeof wishlist_items>;
export const wishlist_item_relations = relations(wishlist_items, ({ one }) => ({
wishlist: one(wishlistsTable, {
@ -30,4 +30,4 @@ export const wishlist_item_relations = relations(wishlist_items, ({ one }) => ({
fields: [wishlist_items.game_id],
references: [gamesTable.id],
}),
}))
}));

View file

@ -0,0 +1,26 @@
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { timestamps } from '../../../common/utils/table';
import { usersTable } from './users.table';
export const wishlistsTable = pgTable('wishlists', {
id: uuid().primaryKey().defaultRandom(),
cuid: text()
.unique()
.$defaultFn(() => cuid2()),
user_id: uuid()
.notNull()
.references(() => usersTable.id, { onDelete: 'cascade' }),
name: text().notNull().default('My Wishlist'),
...timestamps,
});
export type Wishlists = InferSelectModel<typeof wishlistsTable>;
export const wishlists_relations = relations(wishlistsTable, ({ one }) => ({
user: one(usersTable, {
fields: [wishlistsTable.user_id],
references: [usersTable.id],
}),
}));

View file

@ -1,11 +0,0 @@
import type { db } from '../../packages/drizzle'
import * as schema from '../tables'
import roles from './data/roles.json'
export default async function seed(db: db) {
console.log('Creating rolesTable ...')
for (const role of roles) {
await db.insert(schema.rolesTable).values(role).onConflictDoNothing()
}
console.log('Roles created.')
}

View file

@ -1,28 +0,0 @@
import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table'
import { usersTable } from './users.table'
import { collection_items } from './collectionItems.table'
export const collections = pgTable('collections', {
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
.unique()
.$defaultFn(() => cuid2()),
user_id: uuid('user_id')
.notNull()
.references(() => usersTable.id, { onDelete: 'cascade' }),
name: text('name').notNull().default('My Collection'),
...timestamps,
})
export const collection_relations = relations(collections, ({ one, many }) => ({
user: one(usersTable, {
fields: [collections.user_id],
references: [usersTable.id],
}),
collection_items: many(collection_items),
}))
export type Collections = InferSelectModel<typeof collections>

View file

@ -1,23 +0,0 @@
import type { InferSelectModel } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table'
import { usersTable } from './users.table'
export enum CredentialsType {
SECRET = 'secret',
PASSWORD = 'password',
TOTP = 'totp',
HOTP = 'hotp',
}
export const credentialsTable = pgTable('credentials', {
id: uuid('id').primaryKey().defaultRandom(),
user_id: uuid('user_id')
.notNull()
.references(() => usersTable.id, { onDelete: 'cascade' }),
type: text('type').notNull().default(CredentialsType.PASSWORD),
secret_data: text('secret_data').notNull(),
...timestamps,
})
export type Credentials = InferSelectModel<typeof credentialsTable>

View file

@ -1,32 +0,0 @@
import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table.utils'
import { games } from './games'
export const expansions = pgTable('expansions', {
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
.unique()
.$defaultFn(() => cuid2()),
base_game_id: uuid('base_game_id')
.notNull()
.references(() => games.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
game_id: uuid('game_id')
.notNull()
.references(() => games.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
...timestamps,
})
export type Expansions = InferSelectModel<typeof expansions>
export const expansion_relations = relations(expansions, ({ one }) => ({
baseGame: one(games, {
fields: [expansions.base_game_id],
references: [games.id],
}),
game: one(games, {
fields: [expansions.game_id],
references: [games.id],
}),
}))

View file

@ -1,16 +0,0 @@
import { createId as cuid2 } from '@paralleldrive/cuid2'
import type { InferSelectModel } from 'drizzle-orm'
import { pgEnum, pgTable, text, uuid } from 'drizzle-orm/pg-core'
export const externalIdType = pgEnum('external_id_type', ['game', 'category', 'mechanic', 'publisher', 'designer', 'artist'])
export const externalIdsTable = pgTable('external_ids', {
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
.unique()
.$defaultFn(() => cuid2()),
type: externalIdType('type'),
externalId: text('external_id').notNull(),
})
export type ExternalIds = InferSelectModel<typeof externalIdsTable>

View file

@ -1,17 +0,0 @@
import { type InferSelectModel } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table'
import { usersTable } from './users.table'
export const federatedIdentityTable = pgTable('federated_identity', {
id: uuid('id').primaryKey().defaultRandom(),
user_id: uuid('user_id')
.notNull()
.references(() => usersTable.id, { onDelete: 'cascade' }),
identity_provider: text('identity_provider').notNull(),
federated_user_id: text('federated_user_id').notNull(),
federated_username: text('federated_username').notNull(),
...timestamps,
})
export type FederatedIdentity = InferSelectModel<typeof federatedIdentityTable>

View file

@ -1,51 +0,0 @@
import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations, sql } from 'drizzle-orm'
import { index, integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table'
import { categories_to_games_table } from './categoriesToGames.table'
import { gamesToExternalIdsTable } from './gamesToExternalIds.table'
import { mechanics_to_games } from './mechanicsToGames.table'
import { publishers_to_games } from './publishersToGames.table'
export const gamesTable = pgTable(
'games',
{
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
.unique()
.$defaultFn(() => cuid2()),
name: text('name').notNull(),
slug: text('slug').notNull(),
description: text('description'),
year_published: integer('year_published'),
min_players: integer('min_players'),
max_players: integer('max_players'),
playtime: integer('playtime'),
min_playtime: integer('min_playtime'),
max_playtime: integer('max_playtime'),
min_age: integer('min_age'),
image_url: text('image_url'),
thumb_url: text('thumb_url'),
url: text('url'),
last_sync_at: timestamp('last_sync_at'),
...timestamps,
},
(table) => ({
searchIndex: index('search_index').using(
'gin',
sql`(
setweight(to_tsvector('english', ${table.name}), 'A') ||
setweight(to_tsvector('english', ${table.slug}), 'B')
)`,
),
}),
)
export const gameRelations = relations(gamesTable, ({ many }) => ({
categories_to_games: many(categories_to_games_table),
mechanics_to_games: many(mechanics_to_games),
publishers_to_games: many(publishers_to_games),
gamesToExternalIds: many(gamesToExternalIdsTable),
}))
export type Games = InferSelectModel<typeof gamesTable>

View file

@ -1,23 +0,0 @@
import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table'
import { mechanicsToExternalIdsTable } from './mechanicsToExternalIds.table'
import { mechanics_to_games } from './mechanicsToGames.table'
export const mechanicsTable = 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 mechanicsTable>
export const mechanics_relations = relations(mechanicsTable, ({ many }) => ({
mechanics_to_games: many(mechanics_to_games),
mechanicsToExternalIds: many(mechanicsToExternalIdsTable),
}))

View file

@ -1,23 +0,0 @@
import { timestamps } from '../../common/utils/table.utils'
import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { mechanicsToExternalIds } from './mechanicsToExternalIds'
import { mechanics_to_games } from './mechanicsToGames'
export 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(mechanics_to_games),
mechanicsToExternalIds: many(mechanicsToExternalIds),
}))

View file

@ -1,23 +0,0 @@
import { timestamps } from '../../common/utils/table.utils'
import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { publishersToExternalIds } from './publishersToExternalIds'
import { publishers_to_games } from './publishersToGames'
export 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),
}))

View file

@ -1,16 +0,0 @@
import type { InferSelectModel } from 'drizzle-orm'
import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table'
import { usersTable } from './users.table'
export const recoveryCodesTable = pgTable('recovery_codes', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id')
.notNull()
.references(() => usersTable.id),
code: text('code').notNull(),
used: boolean('used').default(false),
...timestamps,
})
export type RecoveryCodesTable = InferSelectModel<typeof recoveryCodesTable>

View file

@ -1,28 +0,0 @@
import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table'
import { user_roles } from './userRoles.table'
export enum RoleName {
ADMIN = 'admin',
EDITOR = 'editor',
MODERATOR = 'moderator',
USER = 'user',
}
export const rolesTable = 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 rolesTable>
export const role_relations = relations(rolesTable, ({ many }) => ({
user_roles: many(user_roles),
}))

View file

@ -1,32 +0,0 @@
import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations } from 'drizzle-orm'
import { boolean, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table'
import { usersTable } from './users.table'
export const twoFactorTable = pgTable('two_factor', {
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
.unique()
.$defaultFn(() => cuid2()),
secret: text('secret').notNull(),
enabled: boolean('enabled').notNull().default(false),
initiatedTime: timestamp('initiated_time', {
mode: 'date',
withTimezone: true,
}),
userId: uuid('user_id')
.notNull()
.references(() => usersTable.id)
.unique(),
...timestamps,
})
export const emailVerificationsRelations = relations(twoFactorTable, ({ one }) => ({
user: one(usersTable, {
fields: [twoFactorTable.userId],
references: [usersTable.id],
}),
}))
export type TwoFactor = InferSelectModel<typeof twoFactorTable>

View file

@ -1,26 +0,0 @@
import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table'
import { usersTable } from './users.table'
export const wishlistsTable = pgTable('wishlists', {
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
.unique()
.$defaultFn(() => cuid2()),
user_id: uuid('user_id')
.notNull()
.references(() => usersTable.id, { onDelete: 'cascade' }),
name: text('name').notNull().default('My Wishlist'),
...timestamps,
})
export type Wishlists = InferSelectModel<typeof wishlistsTable>
export const wishlists_relations = relations(wishlistsTable, ({ one }) => ({
user: one(usersTable, {
fields: [wishlistsTable.user_id],
references: [usersTable.id],
}),
}))

View file

@ -1,4 +1,3 @@
import 'reflect-metadata';
import createApp from '$lib/server/api/common/create-app';
import configureOpenAPI from '$lib/server/api/configure-open-api';
import { CollectionController } from '$lib/server/api/controllers/collection.controller';
@ -34,7 +33,6 @@ const routes = app
.route('/mfa', container.resolve(MfaController).routes())
.get('/', (c) => c.json({ message: 'Server is healthy' }));
// @ts-ignore - this is a workaround for https://github.com/paolostyle/hono-zod-openapi/issues/2
configureOpenAPI(app);
/* -------------------------------------------------------------------------- */

View file

@ -1,12 +1,22 @@
import { LuciaService } from '$lib/server/api/services/lucia.service';
import 'reflect-metadata';
import {
type SessionCookie,
cookieExpiresAt,
cookieName,
createBlankSessionTokenCookie,
createSessionTokenCookie,
setSessionCookie,
} from '$lib/server/api/common/utils/cookies';
import { SessionsService } from '$lib/server/api/services/sessions.service';
import type { MiddlewareHandler } from 'hono';
import { createMiddleware } from 'hono/factory';
import { parseCookies } from 'oslo/cookie';
import { verifyRequestOrigin } from 'oslo/request';
import { container } from 'tsyringe';
import type { AppBindings } from '../common/types/hono';
// resolve dependencies from the container
const { lucia } = container.resolve(LuciaService);
const sessionService = container.resolve(SessionsService);
export const verifyOrigin: MiddlewareHandler<AppBindings> = createMiddleware(async (c, next) => {
if (c.req.method === 'GET') {
@ -21,20 +31,22 @@ export const verifyOrigin: MiddlewareHandler<AppBindings> = createMiddleware(asy
});
export const validateAuthSession: MiddlewareHandler<AppBindings> = createMiddleware(async (c, next) => {
const sessionId = lucia.readSessionCookie(c.req.header('Cookie') ?? '');
const cookies = parseCookies(c.req.header('Cookie') ?? '');
const sessionId = cookies.get(cookieName) ?? null;
if (!sessionId) {
c.set('user', null);
c.set('session', null);
return next();
}
const { session, user } = await lucia.validateSession(sessionId);
if (session?.fresh) {
c.header('Set-Cookie', lucia.createSessionCookie(session.id).serialize(), { append: true });
}
if (!session) {
c.header('Set-Cookie', lucia.createBlankSessionCookie().serialize(), { append: true });
const { session, user } = await sessionService.validateSessionToken(sessionId);
let sessionCookie: SessionCookie;
if (session !== null) {
sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt);
} else {
sessionCookie = createBlankSessionTokenCookie();
}
setSessionCookie(c, sessionCookie);
c.set('session', session);
c.set('user', user);
return next();

View file

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

View file

@ -1,22 +1,24 @@
import { drizzle } from 'drizzle-orm/node-postgres'
import pg from 'pg'
import { config } from '../common/config'
import * as schema from '../databases/tables'
import { drizzle } from 'drizzle-orm/node-postgres';
import pg from 'pg';
import { config } from '../common/config';
import * as schema from '../databases/postgres/tables';
// create the connection
export const pool = new pg.Pool({
user: config.DATABASE_USER,
password: config.DATABASE_PASSWORD,
host: config.DATABASE_HOST,
port: Number(config.DATABASE_PORT).valueOf(),
database: config.DATABASE_DB,
ssl: config.DATABASE_HOST !== 'localhost',
max: config.DB_MIGRATING || config.DB_SEEDING ? 1 : undefined,
})
user: config.postgres.user,
password: config.postgres.password,
host: config.postgres.host,
port: Number(config.postgres.port).valueOf(),
database: config.postgres.database,
ssl: config.postgres.host !== 'localhost',
max: config.postgres.migrating || config.postgres.seeding ? 1 : undefined,
});
export const db = drizzle(pool, {
export const db = drizzle({
client: pool,
casing: 'snake_case',
schema,
logger: config.NODE_ENV === 'development',
})
logger: !config.isProduction,
});
export type db = typeof db
export type db = typeof db;

View file

@ -1,18 +1,18 @@
import { takeFirstOrThrow } from '$lib/server/api/common/utils/repository'
import { DrizzleService } from '$lib/server/api/services/drizzle.service'
import { type InferInsertModel, eq } from 'drizzle-orm'
import { inject, injectable } from 'tsyringe'
import { collections } from '../databases/tables'
import { takeFirstOrThrow } from '$lib/server/api/common/utils/repository';
import { DrizzleService } from '$lib/server/api/services/drizzle.service';
import { type InferInsertModel, eq } from 'drizzle-orm';
import { inject, injectable } from 'tsyringe';
import { collections } from '../databases/postgres/tables';
export type CreateCollection = InferInsertModel<typeof collections>
export type UpdateCollection = Partial<CreateCollection>
export type CreateCollection = InferInsertModel<typeof collections>;
export type UpdateCollection = Partial<CreateCollection>;
@injectable()
export class CollectionsRepository {
constructor(@inject(DrizzleService) private readonly drizzle: DrizzleService) {}
async findAll(db = this.drizzle.db) {
return db.query.collections.findMany()
return db.query.collections.findMany();
}
async findOneById(id: string, db = this.drizzle.db) {
@ -22,7 +22,7 @@ export class CollectionsRepository {
cuid: true,
name: true,
},
})
});
}
async findOneByCuid(cuid: string, db = this.drizzle.db) {
@ -32,7 +32,7 @@ export class CollectionsRepository {
cuid: true,
name: true,
},
})
});
}
async findOneByUserId(userId: string, db = this.drizzle.db) {
@ -42,7 +42,7 @@ export class CollectionsRepository {
cuid: true,
name: true,
},
})
});
}
async findAllByUserId(userId: string, db = this.drizzle.db) {
@ -53,7 +53,7 @@ export class CollectionsRepository {
name: true,
createdAt: true,
},
})
});
}
async findAllByUserIdWithDetails(userId: string, db = this.drizzle.db) {
@ -70,14 +70,14 @@ export class CollectionsRepository {
},
},
},
})
});
}
async create(data: CreateCollection, db = this.drizzle.db) {
return db.insert(collections).values(data).returning().then(takeFirstOrThrow)
return db.insert(collections).values(data).returning().then(takeFirstOrThrow);
}
async update(id: string, data: UpdateCollection, db = this.drizzle.db) {
return db.update(collections).set(data).where(eq(collections.id, id)).returning().then(takeFirstOrThrow)
return db.update(collections).set(data).where(eq(collections.id, id)).returning().then(takeFirstOrThrow);
}
}

View file

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

View file

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

View file

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

View file

@ -1,8 +1,8 @@
import { DrizzleService } from '$lib/server/api/services/drizzle.service'
import { type InferInsertModel, eq } from 'drizzle-orm'
import { inject, injectable } from 'tsyringe'
import { takeFirstOrThrow } from '../common/utils/repository'
import { rolesTable } from '../databases/tables'
import { DrizzleService } from '$lib/server/api/services/drizzle.service';
import { type InferInsertModel, eq } from 'drizzle-orm';
import { inject, injectable } from 'tsyringe';
import { takeFirstOrThrow } from '../common/utils/repository';
import { rolesTable } from '../databases/postgres/tables';
/* -------------------------------------------------------------------------- */
/* Repository */
@ -20,8 +20,8 @@ storing data. They should not contain any business logic, only database queries.
In our case the method 'trxHost' is used to set the transaction context.
*/
export type CreateRole = InferInsertModel<typeof rolesTable>
export type UpdateRole = Partial<CreateRole>
export type CreateRole = InferInsertModel<typeof rolesTable>;
export type UpdateRole = Partial<CreateRole>;
@injectable()
export class RolesRepository {
@ -30,40 +30,40 @@ export class RolesRepository {
async findOneById(id: string, db = this.drizzle.db) {
return db.query.rolesTable.findFirst({
where: eq(rolesTable.id, id),
})
});
}
async findOneByIdOrThrow(id: string, db = this.drizzle.db) {
const role = await this.findOneById(id, db)
if (!role) throw Error('Role not found')
return role
const role = await this.findOneById(id, db);
if (!role) throw Error('Role not found');
return role;
}
async findAll(db = this.drizzle.db) {
return db.query.rolesTable.findMany()
return db.query.rolesTable.findMany();
}
async findOneByName(name: string, db = this.drizzle.db) {
return db.query.rolesTable.findFirst({
where: eq(rolesTable.name, name),
})
});
}
async findOneByNameOrThrow(name: string, db = this.drizzle.db) {
const role = await this.findOneByName(name, db)
if (!role) throw Error('Role not found')
return role
const role = await this.findOneByName(name, db);
if (!role) throw Error('Role not found');
return role;
}
async create(data: CreateRole, db = this.drizzle.db) {
return db.insert(rolesTable).values(data).returning().then(takeFirstOrThrow)
return db.insert(rolesTable).values(data).returning().then(takeFirstOrThrow);
}
async update(id: string, data: UpdateRole, db = this.drizzle.db) {
return db.update(rolesTable).set(data).where(eq(rolesTable.id, id)).returning().then(takeFirstOrThrow)
return db.update(rolesTable).set(data).where(eq(rolesTable.id, id)).returning().then(takeFirstOrThrow);
}
async delete(id: string, db = this.drizzle.db) {
return db.delete(rolesTable).where(eq(rolesTable.id, id)).returning().then(takeFirstOrThrow)
return db.delete(rolesTable).where(eq(rolesTable.id, id)).returning().then(takeFirstOrThrow);
}
}

View file

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

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