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

View file

@ -1,34 +1,34 @@
import { PUBLIC_SITE_URL } from '$env/static/public' import { PUBLIC_SITE_URL } from '$env/static/public';
import { createPasswordResetToken } from '$lib/server/auth-utils.js' import { createPasswordResetToken } from '$lib/server/auth-utils.js';
import { error } from '@sveltejs/kit' import { error } from '@sveltejs/kit';
import { eq } from 'drizzle-orm' import { eq } from 'drizzle-orm';
import { usersTable } from '../../src/lib/server/api/databases/tables' import { usersTable } from '../../src/lib/server/api/databases/postgres/tables';
import { db } from '../../src/lib/server/api/packages/drizzle' import { db } from '../../src/lib/server/api/packages/drizzle';
export async function POST({ locals, request }) { export async function POST({ locals, request }) {
const { email }: { email: string } = await request.json() const { email }: { email: string } = await request.json();
if (!locals.user) { if (!locals.user) {
error(401, { message: 'Unauthorized' }) error(401, { message: 'Unauthorized' });
} }
const user = await db.query.usersTable.findFirst({ const user = await db.query.usersTable.findFirst({
where: eq(usersTable.email, email), where: eq(usersTable.email, email),
}) });
if (!user) { if (!user) {
error(200, { error(200, {
message: 'Email sent! Please check your email for a link to reset your password.', message: 'Email sent! Please check your email for a link to reset your password.',
}) });
} }
const verificationToken = await createPasswordResetToken(user.id) const verificationToken = await createPasswordResetToken(user.id);
const verificationLink = PUBLIC_SITE_URL + verificationToken const verificationLink = PUBLIC_SITE_URL + verificationToken;
// TODO: send email // TODO: send email
console.log('Verification link: ' + verificationLink) console.log('Verification link: ' + verificationLink);
return new Response(null, { return new Response(null, {
status: 200, status: 200,
}) });
} }

View file

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

View file

@ -5,8 +5,8 @@
"scripts": { "scripts": {
"db:push": "drizzle-kit push", "db:push": "drizzle-kit push",
"db:generate": "drizzle-kit generate", "db:generate": "drizzle-kit generate",
"db:migrate": "tsx src/lib/server/api/databases/migrate.ts", "db:migrate": "tsx src/lib/server/api/databases/postgres/migrate.ts",
"db:seed": "tsx src/lib/server/api/databases/seed.ts", "db:seed": "tsx src/lib/server/api/databases/postgres/seed.ts",
"db:studio": "drizzle-kit studio --verbose", "db:studio": "drizzle-kit studio --verbose",
"dev": "NODE_OPTIONS=\"--inspect\" vite dev --host", "dev": "NODE_OPTIONS=\"--inspect\" vite dev --host",
"build": "vite build", "build": "vite build",
@ -27,20 +27,20 @@
"@faker-js/faker": "^8.4.1", "@faker-js/faker": "^8.4.1",
"@melt-ui/pp": "^0.3.2", "@melt-ui/pp": "^0.3.2",
"@melt-ui/svelte": "^0.83.0", "@melt-ui/svelte": "^0.83.0",
"@playwright/test": "^1.47.1", "@playwright/test": "^1.48.2",
"@sveltejs/adapter-auto": "^3.2.5", "@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/enhanced-img": "^0.3.8", "@sveltejs/enhanced-img": "^0.3.10",
"@sveltejs/kit": "^2.6.3", "@sveltejs/kit": "^2.8.0",
"@sveltejs/vite-plugin-svelte": "4.0.0-next.7", "@sveltejs/vite-plugin-svelte": "4.0.0-next.7",
"@types/cookie": "^0.6.0", "@types/cookie": "^0.6.0",
"@types/node": "^20.16.11", "@types/node": "^20.17.6",
"@types/pg": "^8.11.10", "@types/pg": "^8.11.10",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
"@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0", "@typescript-eslint/parser": "^7.18.0",
"arctic": "^1.9.2", "arctic": "^1.9.2",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"drizzle-kit": "^0.23.2", "drizzle-kit": "^0.27.2",
"eslint": "^8.57.1", "eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "2.36.0-next.13", "eslint-plugin-svelte": "2.36.0-next.13",
@ -48,7 +48,7 @@
"just-debounce-it": "^3.2.0", "just-debounce-it": "^3.2.0",
"lucia": "3.2.0", "lucia": "3.2.0",
"lucide-svelte": "^0.408.0", "lucide-svelte": "^0.408.0",
"nodemailer": "^6.9.15", "nodemailer": "^6.9.16",
"postcss": "^8.4.47", "postcss": "^8.4.47",
"postcss-import": "^16.1.0", "postcss-import": "^16.1.0",
"postcss-load-config": "^5.1.0", "postcss-load-config": "^5.1.0",
@ -57,18 +57,18 @@
"prettier-plugin-svelte": "^3.2.7", "prettier-plugin-svelte": "^3.2.7",
"svelte": "5.0.0-next.175", "svelte": "5.0.0-next.175",
"svelte-check": "^3.8.6", "svelte-check": "^3.8.6",
"svelte-headless-table": "^0.18.2", "svelte-headless-table": "^0.18.3",
"svelte-meta-tags": "^3.1.4", "svelte-meta-tags": "^3.1.4",
"svelte-preprocess": "^6.0.3", "svelte-preprocess": "^6.0.3",
"svelte-sequential-preprocessor": "^2.0.2", "svelte-sequential-preprocessor": "^2.0.2",
"sveltekit-flash-message": "^2.4.4", "sveltekit-flash-message": "^2.4.4",
"sveltekit-superforms": "^2.19.1", "sveltekit-superforms": "^2.20.0",
"tailwindcss": "^3.4.13", "tailwindcss": "^3.4.14",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tslib": "^2.7.0", "tslib": "^2.8.1",
"tsx": "^4.19.1", "tsx": "^4.19.2",
"typescript": "^5.6.2", "typescript": "^5.6.3",
"vite": "^5.4.8", "vite": "^5.4.10",
"vitest": "^1.6.0", "vitest": "^1.6.0",
"zod": "^3.23.8" "zod": "^3.23.8"
}, },
@ -83,6 +83,7 @@
"@internationalized/date": "^3.5.6", "@internationalized/date": "^3.5.6",
"@lucia-auth/adapter-drizzle": "^1.1.0", "@lucia-auth/adapter-drizzle": "^1.1.0",
"@lukeed/uuid": "^2.0.1", "@lukeed/uuid": "^2.0.1",
"@needle-di/core": "^0.8.4",
"@neondatabase/serverless": "^0.9.5", "@neondatabase/serverless": "^0.9.5",
"@node-rs/argon2": "^1.8.3", "@node-rs/argon2": "^1.8.3",
"@oslojs/crypto": "^1.0.1", "@oslojs/crypto": "^1.0.1",
@ -92,27 +93,27 @@
"@oslojs/otp": "^1.0.0", "@oslojs/otp": "^1.0.0",
"@oslojs/webauthn": "^1.0.0", "@oslojs/webauthn": "^1.0.0",
"@paralleldrive/cuid2": "^2.2.2", "@paralleldrive/cuid2": "^2.2.2",
"@scalar/hono-api-reference": "^0.5.152", "@scalar/hono-api-reference": "^0.5.159",
"@sveltejs/adapter-node": "^5.2.5", "@sveltejs/adapter-node": "^5.2.9",
"@sveltejs/adapter-vercel": "^5.4.5", "@sveltejs/adapter-vercel": "^5.4.7",
"@types/feather-icons": "^4.29.4", "@types/feather-icons": "^4.29.4",
"bits-ui": "^0.21.16", "bits-ui": "^0.21.16",
"boardgamegeekclient": "^1.9.1", "boardgamegeekclient": "^1.9.1",
"bullmq": "^5.17.1", "bullmq": "^5.25.3",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cookie": "^0.6.0", "cookie": "^1.0.1",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"dotenv-expand": "^11.0.6", "dotenv-expand": "^11.0.6",
"drizzle-orm": "^0.32.2", "drizzle-orm": "^0.36.1",
"drizzle-zod": "^0.5.1", "drizzle-zod": "^0.5.1",
"feather-icons": "^4.29.2", "feather-icons": "^4.29.2",
"formsnap": "^1.0.1", "formsnap": "^1.0.1",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"hono": "^4.6.3", "hono": "^4.6.9",
"hono-pino": "^0.3.0", "hono-pino": "^0.3.0",
"hono-rate-limiter": "^0.4.0", "hono-rate-limiter": "^0.4.0",
"hono-zod-openapi": "^0.2.0", "hono-zod-openapi": "^0.4.2",
"html-entities": "^2.5.2", "html-entities": "^2.5.2",
"iconify-icon": "^2.1.0", "iconify-icon": "^2.1.0",
"ioredis": "^5.4.1", "ioredis": "^5.4.1",
@ -120,23 +121,23 @@
"just-kebab-case": "^4.2.0", "just-kebab-case": "^4.2.0",
"loader": "^2.1.1", "loader": "^2.1.1",
"mode-watcher": "^0.4.1", "mode-watcher": "^0.4.1",
"open-props": "^1.7.6", "open-props": "^1.7.7",
"oslo": "^1.2.1", "oslo": "^1.2.1",
"pg": "^8.13.0", "pg": "^8.13.1",
"pino": "^9.4.0", "pino": "^9.5.0",
"pino-pretty": "^11.2.2", "pino-pretty": "^11.3.0",
"postgres": "^3.4.4", "postgres": "^3.4.5",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"radix-svelte": "^0.9.0", "radix-svelte": "^0.9.0",
"rate-limit-redis": "^4.2.0", "rate-limit-redis": "^4.2.0",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"stoker": "^1.0.9", "stoker": "^1.3.0",
"svelte-lazy-loader": "^1.0.0", "svelte-lazy-loader": "^1.0.0",
"svelte-sonner": "^0.3.28", "svelte-sonner": "^0.3.28",
"tailwind-merge": "^2.5.2", "tailwind-merge": "^2.5.4",
"tailwind-variants": "^0.2.1", "tailwind-variants": "^0.2.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"tsyringe": "^4.8.0", "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, database: env.DATABASE_DB,
ssl: false, // env.DATABASE_HOST !== 'localhost', ssl: false, // env.DATABASE_HOST !== 'localhost',
max: env.DB_MIGRATING || env.DB_SEEDING ? 1 : undefined, 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 { export interface Config {
isProduction: boolean isProduction: boolean;
domain: string domain: string;
api: ApiConfig api: ApiConfig;
// storage: StorageConfig // storage: StorageConfig
redis: RedisConfig redis: RedisConfig;
postgres: PostgresConfig postgres: PostgresConfig;
} }
interface ApiConfig { interface ApiConfig {
origin: string origin: string;
} }
// interface StorageConfig { // interface StorageConfig {
@ -19,15 +19,17 @@ interface ApiConfig {
// } // }
interface RedisConfig { interface RedisConfig {
url: string url: string;
} }
interface PostgresConfig { interface PostgresConfig {
user: string user: string;
password: string password: string;
host: string host: string;
port: number port: number;
database: string database: string;
ssl: boolean ssl: boolean;
max: number | undefined 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 { Hono } from 'hono';
import type { PinoLogger } from 'hono-pino'; import type { PinoLogger } from 'hono-pino';
import type { Promisify, RateLimitInfo } from 'hono-rate-limiter'; import type { Promisify, RateLimitInfo } from 'hono-rate-limiter';
import type { Session, User } from 'lucia'; import type { User } from 'lucia';
// export type AppOpenAPI = OpenAPIHono<AppBindings>; // export type AppOpenAPI = OpenAPIHono<AppBindings>;
export type AppOpenAPI = Hono; export type AppOpenAPI = Hono<AppBindings>;
export type AppBindings = { export type AppBindings = {
Variables: { Variables: {
logger: PinoLogger; logger: PinoLogger;
session: Session | null; session: Sessions | null;
user: User | null; user: User | null;
rateLimit: RateLimitInfo; rateLimit: RateLimitInfo;
rateLimitStore: { rateLimitStore: {
@ -22,7 +23,7 @@ export type AppBindings = {
export type HonoTypes = { export type HonoTypes = {
Variables: { Variables: {
logger: PinoLogger; logger: PinoLogger;
session: Session | null; session: Sessions | null;
user: User | null; user: User | null;
rateLimit: RateLimitInfo; rateLimit: RateLimitInfo;
rateLimitStore: { 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 { timestamp } from 'drizzle-orm/pg-core';
import { customType } from 'drizzle-orm/pg-core' import { customType } from 'drizzle-orm/pg-core';
export const citext = customType<{ data: string }>({ export const citext = customType<{ data: string }>({
dataType() { dataType() {
return 'citext' return 'citext';
}, },
}) });
export const cuid2 = customType<{ data: string }>({ export const cuid2 = customType<{ data: string }>({
dataType() { dataType() {
return 'text' return 'text';
}, },
}) });
export const timestamps = { export const timestamps = {
createdAt: timestamp('created_at', { createdAt: timestamp('created_at', {
@ -25,5 +25,6 @@ export const timestamps = {
withTimezone: true, withTimezone: true,
}) })
.notNull() .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 { apiReference } from '@scalar/hono-api-reference';
import { Hono } from 'hono';
import type { AppOpenAPI } from '$lib/server/api/common/types/hono'; import type { AppOpenAPI } from '$lib/server/api/common/types/hono';
// import { createOpenApiDocument } from 'hono-zod-openapi'; // import { createOpenApiDocument } from 'hono-zod-openapi';
@ -40,6 +38,19 @@ export default function configureOpenAPI(app: AppOpenAPI) {
description: 'Bored Game API', description: 'Bored Game API',
version: packageJSON.version, version: packageJSON.version,
}, },
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
},
cookieAuth: {
type: 'apiKey',
name: 'session',
in: 'cookie',
}
},
},
}); });
app.get( app.get(

View file

@ -1,32 +1,40 @@
import 'reflect-metadata' import 'reflect-metadata';
import { Controller } from '$lib/server/api/common/types/controller' import { StatusCodes } from '$lib/constants/status-codes';
import { CollectionsService } from '$lib/server/api/services/collections.service' import { Controller } from '$lib/server/api/common/types/controller';
import { inject, injectable } from 'tsyringe' import { allCollections, getCollectionByCUID, numberOfCollections } from '$lib/server/api/controllers/collection.routes';
import { requireAuth } from '../middleware/require-auth.middleware' 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() @injectable()
export class CollectionController extends Controller { export class CollectionController extends Controller {
constructor(@inject(CollectionsService) private readonly collectionsService: CollectionsService) { constructor(@inject(CollectionsService) private readonly collectionsService: CollectionsService) {
super() super();
} }
routes() { routes() {
return this.controller return this.controller
.get('/', requireAuth, async (c) => { .get('/', requireAuth, openApi(allCollections), async (c) => {
const user = c.var.user const user = c.var.user;
const collections = await this.collectionsService.findAllByUserId(user.id) const collections = await this.collectionsService.findAllByUserId(user.id);
console.log('collections service', collections) console.log('collections service', collections);
return c.json({ collections }) return c.json({ collections }, StatusCodes.OK);
}) })
.get('/count', requireAuth, async (c) => { .get('/count', requireAuth, openApi(numberOfCollections), async (c) => {
const user = c.var.user const user = c.var.user;
const collections = await this.collectionsService.findAllByUserIdWithDetails(user.id) const collections = await this.collectionsService.findAllByUserIdWithDetails(user.id);
return c.json({ collections }) return c.json({ count: collections?.length || 0 }, StatusCodes.OK);
})
.get('/:cuid', requireAuth, async (c) => {
const cuid = c.req.param('cuid')
const collection = await this.collectionsService.findOneByCuid(cuid)
return c.json({ collection })
}) })
.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 { StatusCodes } from '$lib/constants/status-codes';
import { Controller } from '$lib/server/api/common/types/controller'; 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 { changePasswordDto } from '$lib/server/api/dtos/change-password.dto';
import { updateEmailDto } from '$lib/server/api/dtos/update-email.dto'; import { updateEmailDto } from '$lib/server/api/dtos/update-email.dto';
import { updateProfileDto } from '$lib/server/api/dtos/update-profile.dto'; import { updateProfileDto } from '$lib/server/api/dtos/update-profile.dto';
@ -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 { limiter } from '$lib/server/api/middleware/rate-limiter.middleware';
import { IamService } from '$lib/server/api/services/iam.service'; import { IamService } from '$lib/server/api/services/iam.service';
import { LoginRequestsService } from '$lib/server/api/services/loginrequest.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 { zValidator } from '@hono/zod-validator';
import { openApi } from 'hono-zod-openapi'; import { openApi } from 'hono-zod-openapi';
import { setCookie } from 'hono/cookie'; import { setCookie } from 'hono/cookie';
import { inject, injectable } from 'tsyringe'; import { inject, injectable } from 'tsyringe';
import { requireAuth } from '../middleware/require-auth.middleware'; import { requireAuth } from '../middleware/require-auth.middleware';
import { iam, logout, updateEmail, updatePassword, updateProfile, verifyPassword } from './iam.routes';
@injectable() @injectable()
export class IamController extends Controller { export class IamController extends Controller {
constructor( constructor(
@inject(IamService) private readonly iamService: IamService, @inject(IamService) private readonly iamService: IamService,
@inject(LoginRequestsService) private readonly loginRequestService: LoginRequestsService, @inject(LoginRequestsService) private readonly loginRequestService: LoginRequestsService,
@inject(LuciaService) private luciaService: LuciaService, @inject(SessionsService) private sessionsService: SessionsService,
) { ) {
super(); super();
} }
@ -42,69 +43,74 @@ export class IamController extends Controller {
const { firstName, lastName, username } = c.req.valid('json'); const { firstName, lastName, username } = c.req.valid('json');
const updatedUser = await this.iamService.updateProfile(user.id, { firstName, lastName, username }); const updatedUser = await this.iamService.updateProfile(user.id, { firstName, lastName, username });
if (!updatedUser) { 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); return c.json({ user: updatedUser }, StatusCodes.OK);
}, },
) )
.post('/verify/password', requireAuth, zValidator('json', verifyPasswordDto), limiter({ limit: 10, minutes: 60 }), async (c) => { .post(
const user = c.var.user; '/verify/password',
const { password } = c.req.valid('json'); requireAuth,
const passwordVerified = await this.iamService.verifyPassword(user.id, { password }); zValidator('json', verifyPasswordDto),
if (!passwordVerified) { openApi(verifyPassword),
console.log('Incorrect password'); limiter({ limit: 10, minutes: 60 }),
return c.json('Incorrect password', StatusCodes.BAD_REQUEST); async (c) => {
} const user = c.var.user;
return c.json({}, StatusCodes.OK); const { password } = c.req.valid('json');
}) const passwordVerified = await this.iamService.verifyPassword(user.id, { password });
.put('/update/password', requireAuth, zValidator('json', changePasswordDto), limiter({ limit: 10, minutes: 60 }), async (c) => { if (!passwordVerified) {
const user = c.var.user; console.log('Incorrect password');
const { password, confirm_password } = c.req.valid('json'); return c.json('Incorrect password', StatusCodes.FORBIDDEN);
if (password !== confirm_password) { }
return c.json('Passwords do not match', StatusCodes.BAD_REQUEST); return c.json({}, StatusCodes.OK);
} },
try { )
await this.iamService.updatePassword(user.id, { password, confirm_password }); .put(
await this.luciaService.lucia.invalidateUserSessions(user.id); '/update/password',
await this.loginRequestService.createUserSession(user.id, c.req, undefined); requireAuth,
const sessionCookie = this.luciaService.lucia.createBlankSessionCookie(); openApi(updatePassword),
setCookie(c, sessionCookie.name, sessionCookie.value, { zValidator('json', changePasswordDto),
path: sessionCookie.attributes.path, limiter({ limit: 10, minutes: 60 }),
maxAge: sessionCookie.attributes.maxAge, async (c) => {
domain: sessionCookie.attributes.domain, const user = c.var.user;
sameSite: sessionCookie.attributes.sameSite as any, const { password, confirm_password } = c.req.valid('json');
secure: sessionCookie.attributes.secure, if (password !== confirm_password) {
httpOnly: sessionCookie.attributes.httpOnly, return c.json('Passwords do not match', StatusCodes.UNPROCESSABLE_ENTITY);
expires: sessionCookie.attributes.expires, }
}); try {
return c.json({ status: 'success' }); await this.iamService.updatePassword(user.id, { password, confirm_password });
} catch (error) { await this.sessionsService.invalidateSession(user.id);
console.error('Error updating password', error); await this.loginRequestService.createUserSession(user.id, c.req, undefined);
return c.json('Error updating password', StatusCodes.BAD_REQUEST); const sessionCookie = createBlankSessionTokenCookie();
} setSessionCookie(c, sessionCookie);
}) return c.json({ status: 'success' });
.post('/update/email', requireAuth, zValidator('json', updateEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => { } catch (error) {
const user = c.var.user; console.error('Error updating password', error);
const { email } = c.req.valid('json'); return c.json('Error updating password', StatusCodes.INTERNAL_SERVER_ERROR);
const updatedUser = await this.iamService.updateEmail(user.id, { email }); }
if (!updatedUser) { },
return c.json('Email already in use', StatusCodes.BAD_REQUEST); )
} .post(
return c.json({ user: updatedUser }, StatusCodes.OK); '/update/email',
}) requireAuth,
.post('/logout', requireAuth, async (c) => { 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; const sessionId = c.var.session.id;
await this.iamService.logout(sessionId); await this.iamService.logout(sessionId);
const sessionCookie = this.luciaService.lucia.createBlankSessionCookie(); const sessionCookie = createBlankSessionTokenCookie();
setCookie(c, sessionCookie.name, sessionCookie.value, { setSessionCookie(c, sessionCookie);
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' }); return c.json({ status: 'success' });
}); });
} }

View file

@ -1,14 +1,15 @@
import { StatusCodes } from '$lib/constants/status-codes'; import { StatusCodes } from '$lib/constants/status-codes';
import { unauthorizedSchema } from '$lib/server/api/common/exceptions'; 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 { updateProfileDto } from '$lib/server/api/dtos/update-profile.dto';
import { defineOpenApiOperation } from 'hono-zod-openapi';
import { createErrorSchema } from 'stoker/openapi/schemas'; 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({ export const iam = taggedAuthRoute(tag, {
tags,
responses: { responses: {
[StatusCodes.OK]: { [StatusCodes.OK]: {
description: 'User profile', description: 'User profile',
@ -23,8 +24,7 @@ export const iam = defineOpenApiOperation({
}, },
}); });
export const updateProfile = defineOpenApiOperation({ export const updateProfile = taggedAuthRoute(tag, {
tags,
request: { request: {
json: updateProfileDto, json: updateProfileDto,
}, },
@ -34,11 +34,108 @@ export const updateProfile = defineOpenApiOperation({
schema: selectUserSchema, schema: selectUserSchema,
mediaType: 'application/json', mediaType: 'application/json',
}, },
[StatusCodes.UNPROCESSABLE_ENTITY]: { [StatusCodes.BAD_REQUEST]: {
description: 'The validation error(s)', description: 'The validation error(s)',
schema: createErrorSchema(updateProfileDto), schema: createErrorSchema(updateProfileDto),
mediaType: 'application/json', 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]: { [StatusCodes.UNAUTHORIZED]: {
description: 'Unauthorized', description: 'Unauthorized',
schema: createErrorSchema(unauthorizedSchema), schema: createErrorSchema(unauthorizedSchema),

View file

@ -1,42 +1,38 @@
import 'reflect-metadata' import 'reflect-metadata';
import { Controller } from '$lib/server/api/common/types/controller' import { Controller } from '$lib/server/api/common/types/controller';
import { signinUsernameDto } from '$lib/server/api/dtos/signin-username.dto' import { cookieExpiresAt, createSessionTokenCookie, setSessionCookie } from '$lib/server/api/common/utils/cookies';
import { LuciaService } from '$lib/server/api/services/lucia.service' import { signinUsernameDto } from '$lib/server/api/dtos/signin-username.dto';
import { zValidator } from '@hono/zod-validator' import { SessionsService } from '$lib/server/api/services/sessions.service';
import { setCookie } from 'hono/cookie' import { zValidator } from '@hono/zod-validator';
import { TimeSpan } from 'oslo' import { openApi } from 'hono-zod-openapi';
import { inject, injectable } from 'tsyringe' import { inject, injectable } from 'tsyringe';
import { limiter } from '../middleware/rate-limiter.middleware' import { limiter } from '../middleware/rate-limiter.middleware';
import { LoginRequestsService } from '../services/loginrequest.service' import { LoginRequestsService } from '../services/loginrequest.service';
import { signinUsername } from './login.routes';
@injectable() @injectable()
export class LoginController extends Controller { export class LoginController extends Controller {
constructor( constructor(
@inject(LoginRequestsService) private readonly loginRequestsService: LoginRequestsService, @inject(LoginRequestsService) private readonly loginRequestsService: LoginRequestsService,
@inject(LuciaService) private luciaService: LuciaService, @inject(SessionsService) private luciaService: SessionsService,
) { ) {
super() super();
} }
routes() { routes() {
return this.controller.post('/', zValidator('json', signinUsernameDto), limiter({ limit: 10, minutes: 60 }), async (c) => { return this.controller.post(
const { username, password } = c.req.valid('json') '/',
const session = await this.loginRequestsService.verify({ username, password }, c.req) openApi(signinUsername),
const sessionCookie = this.luciaService.lucia.createSessionCookie(session.id) zValidator('json', signinUsernameDto),
console.log('set cookie', sessionCookie) limiter({ limit: 10, minutes: 60 }),
setCookie(c, sessionCookie.name, sessionCookie.value, { async (c) => {
path: sessionCookie.attributes.path, const { username, password } = c.req.valid('json');
maxAge: const session = await this.loginRequestsService.verify({ username, password }, c.req);
sessionCookie?.attributes?.maxAge && sessionCookie?.attributes?.maxAge < new TimeSpan(365, 'd').seconds() const sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt);
? sessionCookie.attributes.maxAge console.log('set cookie', sessionCookie);
: new TimeSpan(2, 'w').seconds(), setSessionCookie(c, sessionCookie);
domain: sessionCookie.attributes.domain, return c.json({ message: 'ok' });
sameSite: sessionCookie.attributes.sameSite as any, },
secure: sessionCookie.attributes.secure, );
httpOnly: sessionCookie.attributes.httpOnly,
expires: sessionCookie.attributes.expires,
})
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 'reflect-metadata';
import { StatusCodes } from '$lib/constants/status-codes' import { StatusCodes } from '$lib/constants/status-codes';
import { Controller } from '$lib/server/api/common/types/controller' import { Controller } from '$lib/server/api/common/types/controller';
import { verifyTotpDto } from '$lib/server/api/dtos/verify-totp.dto' import { verifyTotpDto } from '$lib/server/api/dtos/verify-totp.dto';
import { RecoveryCodesService } from '$lib/server/api/services/recovery-codes.service' import { RecoveryCodesService } from '$lib/server/api/services/recovery-codes.service';
import { TotpService } from '$lib/server/api/services/totp.service' import { TotpService } from '$lib/server/api/services/totp.service';
import { UsersService } from '$lib/server/api/services/users.service' import { UsersService } from '$lib/server/api/services/users.service';
import { zValidator } from '@hono/zod-validator' import { zValidator } from '@hono/zod-validator';
import { inject, injectable } from 'tsyringe' import { inject, injectable } from 'tsyringe';
import { CredentialsType } from '../databases/tables' import { CredentialsType } from '../databases/postgres/tables';
import { requireAuth } from '../middleware/require-auth.middleware' import { requireAuth } from '../middleware/require-auth.middleware';
@injectable() @injectable()
export class MfaController extends Controller { export class MfaController extends Controller {
@ -17,59 +17,59 @@ export class MfaController extends Controller {
@inject(TotpService) private readonly totpService: TotpService, @inject(TotpService) private readonly totpService: TotpService,
@inject(UsersService) private readonly usersService: UsersService, @inject(UsersService) private readonly usersService: UsersService,
) { ) {
super() super();
} }
routes() { routes() {
return this.controller return this.controller
.get('/totp', requireAuth, async (c) => { .get('/totp', requireAuth, async (c) => {
const user = c.var.user const user = c.var.user;
const totpCredential = await this.totpService.findOneByUserId(user.id) const totpCredential = await this.totpService.findOneByUserId(user.id);
return c.json({ totpCredential }) return c.json({ totpCredential });
}) })
.post('/totp', requireAuth, async (c) => { .post('/totp', requireAuth, async (c) => {
const user = c.var.user const user = c.var.user;
const totpCredential = await this.totpService.create(user.id) const totpCredential = await this.totpService.create(user.id);
return c.json({ totpCredential }) return c.json({ totpCredential });
}) })
.delete('/totp', requireAuth, async (c) => { .delete('/totp', requireAuth, async (c) => {
const user = c.var.user const user = c.var.user;
try { try {
await this.totpService.deleteOneByUserIdAndType(user.id, CredentialsType.TOTP) await this.totpService.deleteOneByUserIdAndType(user.id, CredentialsType.TOTP);
await this.recoveryCodesService.deleteAllRecoveryCodesByUserId(user.id) await this.recoveryCodesService.deleteAllRecoveryCodesByUserId(user.id);
await this.usersService.updateUser(user.id, { mfa_enabled: false }) await this.usersService.updateUser(user.id, { mfa_enabled: false });
console.log('TOTP deleted') console.log('TOTP deleted');
return c.body(null, StatusCodes.NO_CONTENT) return c.body(null, StatusCodes.NO_CONTENT);
} catch (e) { } catch (e) {
console.error(e) console.error(e);
return c.status(StatusCodes.INTERNAL_SERVER_ERROR) return c.status(StatusCodes.INTERNAL_SERVER_ERROR);
} }
}) })
.get('/totp/recoveryCodes', requireAuth, async (c) => { .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 // 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) { if (existingCodes && existingCodes.length > 0) {
console.log('Recovery Codes found', existingCodes) console.log('Recovery Codes found', existingCodes);
return c.json({ recoveryCodes: existingCodes }) return c.json({ recoveryCodes: existingCodes });
} }
const recoveryCodes = await this.recoveryCodesService.createRecoveryCodes(user.id) const recoveryCodes = await this.recoveryCodesService.createRecoveryCodes(user.id);
return c.json({ recoveryCodes }) return c.json({ recoveryCodes });
}) })
.post('/totp/verify', requireAuth, zValidator('json', verifyTotpDto), async (c) => { .post('/totp/verify', requireAuth, zValidator('json', verifyTotpDto), async (c) => {
try { try {
const user = c.var.user const user = c.var.user;
const { code } = c.req.valid('json') const { code } = c.req.valid('json');
const verified = await this.totpService.verify(user.id, code) const verified = await this.totpService.verify(user.id, code);
if (verified) { if (verified) {
await this.usersService.updateUser(user.id, { mfa_enabled: true }) await this.usersService.updateUser(user.id, { mfa_enabled: true });
return c.json({}, StatusCodes.OK) return c.json({}, StatusCodes.OK);
} }
return c.json('Invalid code', StatusCodes.BAD_REQUEST) return c.json('Invalid code', StatusCodes.BAD_REQUEST);
} catch (e) { } catch (e) {
console.error(e) console.error(e);
return c.status(StatusCodes.INTERNAL_SERVER_ERROR) return c.status(StatusCodes.INTERNAL_SERVER_ERROR);
} }
}) });
} }
} }

View file

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

View file

@ -1,43 +1,43 @@
import 'reflect-metadata' import 'reflect-metadata';
import { Controller } from '$lib/server/api/common/types/controller' import { Controller } from '$lib/server/api/common/types/controller';
import { signupUsernameEmailDto } from '$lib/server/api/dtos/signup-username-email.dto' import { signupUsernameEmailDto } from '$lib/server/api/dtos/signup-username-email.dto';
import { limiter } from '$lib/server/api/middleware/rate-limiter.middleware' import { limiter } from '$lib/server/api/middleware/rate-limiter.middleware';
import { LoginRequestsService } from '$lib/server/api/services/loginrequest.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 { UsersService } from '$lib/server/api/services/users.service' import { UsersService } from '$lib/server/api/services/users.service';
import { zValidator } from '@hono/zod-validator' import { zValidator } from '@hono/zod-validator';
import { setCookie } from 'hono/cookie' import { setCookie } from 'hono/cookie';
import { TimeSpan } from 'oslo' import { TimeSpan } from 'oslo';
import { inject, injectable } from 'tsyringe' import { inject, injectable } from 'tsyringe';
@injectable() @injectable()
export class SignupController extends Controller { export class SignupController extends Controller {
constructor( constructor(
@inject(UsersService) private readonly usersService: UsersService, @inject(UsersService) private readonly usersService: UsersService,
@inject(LoginRequestsService) private readonly loginRequestService: LoginRequestsService, @inject(LoginRequestsService) private readonly loginRequestService: LoginRequestsService,
@inject(LuciaService) private luciaService: LuciaService, @inject(SessionsService) private luciaService: SessionsService,
) { ) {
super() super();
} }
routes() { routes() {
return this.controller.post('/', zValidator('json', signupUsernameEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => { 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 { firstName, lastName, email, username, password, confirm_password } = await c.req.valid('json');
const existingUser = await this.usersService.findOneByUsername(username) const existingUser = await this.usersService.findOneByUsername(username);
if (existingUser) { 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) { 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 session = await this.loginRequestService.createUserSession(user.id, c.req, undefined);
const sessionCookie = this.luciaService.lucia.createSessionCookie(session.id) const sessionCookie = this.luciaService.lucia.createSessionCookie(session.id);
console.log('set cookie', sessionCookie) console.log('set cookie', sessionCookie);
setCookie(c, sessionCookie.name, sessionCookie.value, { setCookie(c, sessionCookie.name, sessionCookie.value, {
path: sessionCookie.attributes.path, path: sessionCookie.attributes.path,
maxAge: maxAge:
@ -49,8 +49,8 @@ export class SignupController extends Controller {
secure: sessionCookie.attributes.secure, secure: sessionCookie.attributes.secure,
httpOnly: sessionCookie.attributes.httpOnly, httpOnly: sessionCookie.attributes.httpOnly,
expires: sessionCookie.attributes.expires, 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 { collections } from '$lib/server/api/databases/postgres/tables';
import { createInsertSchema, createSelectSchema } from 'drizzle-zod' import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
import type { z } from 'zod' import type { z } from 'zod';
export const InsertCollectionSchema = createInsertSchema(collections, { export const InsertCollectionSchema = createInsertSchema(collections, {
name: (schema) => name: (schema) =>
@ -10,15 +10,15 @@ export const InsertCollectionSchema = createInsertSchema(collections, {
cuid: true, cuid: true,
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true,
}) });
export type InsertCollectionSchema = z.infer<typeof InsertCollectionSchema> export type InsertCollectionSchema = z.infer<typeof InsertCollectionSchema>;
export const SelectCollectionSchema = createSelectSchema(collections).omit({ export const SelectCollectionSchema = createSelectSchema(collections).omit({
id: true, id: true,
user_id: true, user_id: true,
createdAt: true, createdAt: true,
updatedAt: 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 { usersTable } from '$lib/server/api/databases/postgres/tables';
import { createInsertSchema, createSelectSchema } from 'drizzle-zod' import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
import type { z } from 'zod' import type { z } from 'zod';
export const InsertUserSchema = createInsertSchema(usersTable, { export const InsertUserSchema = createInsertSchema(usersTable, {
email: (schema) => schema.email.max(64).email().optional(), email: (schema) => schema.email.max(64).email().optional(),
@ -15,10 +15,10 @@ export const InsertUserSchema = createInsertSchema(usersTable, {
cuid: true, cuid: true,
createdAt: true, createdAt: true,
updatedAt: 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 Table, getTableName, sql } from 'drizzle-orm' import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
import type { NodePgDatabase } from 'drizzle-orm/node-postgres' import env from '../../common/env';
import env from '../common/env' import { DrizzleService } from '../../services/drizzle.service';
import { DrizzleService } from '../services/drizzle.service' import * as seeds from './seeds';
import * as seeds from './seeds' import * as schema from './tables';
import * as schema from './tables'
const drizzleService = new DrizzleService() const drizzleService = new DrizzleService();
if (!env.DB_SEEDING) { if (!env.DB_SEEDING) {
throw new Error('You must set DB_SEEDING to "true" when running seeds') throw new Error('You must set DB_SEEDING to "true" when running seeds');
} }
async function resetTable(db: NodePgDatabase<typeof schema>, table: Table) { async function resetTable(db: NodePgDatabase<typeof schema>, table: Table) {
return db.execute(sql.raw(`TRUNCATE TABLE ${getTableName(table)} RESTART IDENTITY CASCADE`)) return db.execute(sql.raw(`TRUNCATE TABLE ${getTableName(table)} RESTART IDENTITY CASCADE`));
} }
for (const table of [ for (const table of [
@ -45,11 +44,11 @@ for (const table of [
schema.wishlistsTable, schema.wishlistsTable,
]) { ]) {
// await db.delete(table); // clear tables without truncating / resetting ids // await db.delete(table); // clear tables without truncating / resetting ids
await resetTable(drizzleService.db, table) await resetTable(drizzleService.db, table);
} }
await seeds.roles(drizzleService.db) await seeds.roles(drizzleService.db);
await seeds.users(drizzleService.db) await seeds.users(drizzleService.db);
await drizzleService.dispose() await drizzleService.dispose();
process.exit() 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 { eq } from 'drizzle-orm';
import type { db } from '../../packages/drizzle'; import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { HashingService } from '../../services/hashing.service'; import { HashingService } from '../../../services/hashing.service';
import * as schema from '../tables'; import * as schema from '../tables';
import users from './data/users.json'; import users from './data/users.json';
@ -9,7 +9,7 @@ type JsonRole = {
primary: boolean; primary: boolean;
}; };
export default async function seed(db: db) { export default async function seed(db: NodePgDatabase<typeof schema>) {
const hashingService = new HashingService(); const hashingService = new HashingService();
const adminRole = await db.select().from(schema.rolesTable).where(eq(schema.rolesTable.name, 'admin')); const adminRole = await db.select().from(schema.rolesTable).where(eq(schema.rolesTable.name, 'admin'));
const userRole = await db.select().from(schema.rolesTable).where(eq(schema.rolesTable.name, 'user')); const userRole = await db.select().from(schema.rolesTable).where(eq(schema.rolesTable.name, 'user'));

View file

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

View file

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

View file

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

View file

@ -1,26 +1,26 @@
import { createId as cuid2 } from '@paralleldrive/cuid2' import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm' import { type InferSelectModel, relations } from 'drizzle-orm';
import { integer, pgTable, text, uuid } from 'drizzle-orm/pg-core' import { integer, pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { timestamps } from '../../common/utils/table' import { timestamps } from '../../../common/utils/table';
import { gamesTable } from '././games.table' import { collections } from './collections.table';
import { collections } from './collections.table' import { gamesTable } from './games.table';
export const collection_items = pgTable('collection_items', { export const collection_items = pgTable('collection_items', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid().primaryKey().defaultRandom(),
cuid: text('cuid') cuid: text()
.unique() .unique()
.$defaultFn(() => cuid2()), .$defaultFn(() => cuid2()),
collection_id: uuid('collection_id') collection_id: uuid()
.notNull() .notNull()
.references(() => collections.id, { onDelete: 'cascade' }), .references(() => collections.id, { onDelete: 'cascade' }),
game_id: uuid('game_id') game_id: uuid()
.notNull() .notNull()
.references(() => gamesTable.id, { onDelete: 'cascade' }), .references(() => gamesTable.id, { onDelete: 'cascade' }),
times_played: integer('times_played').default(0), times_played: integer().default(0),
...timestamps, ...timestamps,
}) });
export type CollectionItemsTable = InferSelectModel<typeof collection_items> export type CollectionItemsTable = InferSelectModel<typeof collection_items>;
export const collection_item_relations = relations(collection_items, ({ one }) => ({ export const collection_item_relations = relations(collection_items, ({ one }) => ({
collection: one(collections, { collection: one(collections, {
@ -31,4 +31,4 @@ export const collection_item_relations = relations(collection_items, ({ one }) =
fields: [collection_items.game_id], fields: [collection_items.game_id],
references: [gamesTable.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 { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm' import { type InferSelectModel, relations } from 'drizzle-orm';
import { pgTable, text, uuid } from 'drizzle-orm/pg-core' import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { timestamps } from '../../common/utils/table' import { timestamps } from '../../../common/utils/table';
import { gamesTable } from '././games.table' import { gamesTable } from './games.table';
export const expansionsTable = pgTable('expansions', { export const expansionsTable = pgTable('expansions', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid().primaryKey().defaultRandom(),
cuid: text('cuid') cuid: text()
.unique() .unique()
.$defaultFn(() => cuid2()), .$defaultFn(() => cuid2()),
base_game_id: uuid('base_game_id') base_game_id: uuid()
.notNull() .notNull()
.references(() => gamesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }), .references(() => gamesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
game_id: uuid('game_id') game_id: uuid()
.notNull() .notNull()
.references(() => gamesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }), .references(() => gamesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
...timestamps, ...timestamps,
}) });
export type Expansions = InferSelectModel<typeof expansionsTable> export type Expansions = InferSelectModel<typeof expansionsTable>;
export const expansion_relations = relations(expansionsTable, ({ one }) => ({ export const expansion_relations = relations(expansionsTable, ({ one }) => ({
baseGame: one(gamesTable, { baseGame: one(gamesTable, {
@ -29,4 +29,4 @@ export const expansion_relations = relations(expansionsTable, ({ one }) => ({
fields: [expansionsTable.game_id], fields: [expansionsTable.game_id],
references: [gamesTable.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 { relations } from 'drizzle-orm';
import { gamesTable } from '././games.table' import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core';
import { externalIdsTable } from './externalIds.table' import { externalIdsTable } from './externalIds.table';
import { relations } from 'drizzle-orm' import { gamesTable } from './games.table';
export const gamesToExternalIdsTable = pgTable( export const gamesToExternalIdsTable = pgTable(
'games_to_external_ids', 'games_to_external_ids',
{ {
gameId: uuid('game_id') gameId: uuid()
.notNull() .notNull()
.references(() => gamesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }), .references(() => gamesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
externalId: uuid('external_id') externalId: uuid()
.notNull() .notNull()
.references(() => externalIdsTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }), .references(() => externalIdsTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
}, },
@ -18,9 +18,9 @@ export const gamesToExternalIdsTable = pgTable(
gamesToExternalIdsPkey: primaryKey({ gamesToExternalIdsPkey: primaryKey({
columns: [table.gameId, table.externalId], columns: [table.gameId, table.externalId],
}), }),
} };
}, },
) );
export const gamesToExternalIdsRelations = relations(gamesToExternalIdsTable, ({ one }) => ({ export const gamesToExternalIdsRelations = relations(gamesToExternalIdsTable, ({ one }) => ({
game: one(gamesTable, { game: one(gamesTable, {
@ -31,4 +31,4 @@ export const gamesToExternalIdsRelations = relations(gamesToExternalIdsTable, ({
fields: [gamesToExternalIdsTable.externalId], fields: [gamesToExternalIdsTable.externalId],
references: [externalIdsTable.id], 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 { relations } from 'drizzle-orm';
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core' import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core';
import { gamesTable } from '././games.table' import { gamesTable } from './games.table';
import { mechanicsTable } from './mechanics.table' import { mechanicsTable } from './mechanics.table';
export const mechanics_to_games = pgTable( export const mechanics_to_games = pgTable(
'mechanics_to_games', 'mechanics_to_games',
{ {
mechanic_id: uuid('mechanic_id') mechanic_id: uuid()
.notNull() .notNull()
.references(() => mechanicsTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }), .references(() => mechanicsTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
game_id: uuid('game_id') game_id: uuid()
.notNull() .notNull()
.references(() => gamesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }), .references(() => gamesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
}, },
@ -18,9 +18,9 @@ export const mechanics_to_games = pgTable(
mechanicsToGamesPkey: primaryKey({ mechanicsToGamesPkey: primaryKey({
columns: [table.mechanic_id, table.game_id], columns: [table.mechanic_id, table.game_id],
}), }),
} };
}, },
) );
export const mechanics_to_games_relations = relations(mechanics_to_games, ({ one }) => ({ export const mechanics_to_games_relations = relations(mechanics_to_games, ({ one }) => ({
mechanic: one(mechanicsTable, { mechanic: one(mechanicsTable, {
@ -31,4 +31,4 @@ export const mechanics_to_games_relations = relations(mechanics_to_games, ({ one
fields: [mechanics_to_games.game_id], fields: [mechanics_to_games.game_id],
references: [gamesTable.id], references: [gamesTable.id],
}), }),
})) }));

View file

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

View file

@ -1,23 +1,23 @@
import { createId as cuid2 } from '@paralleldrive/cuid2' import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm' import { type InferSelectModel, relations } from 'drizzle-orm';
import { pgTable, text, uuid } from 'drizzle-orm/pg-core' import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { timestamps } from '../../common/utils/table' import { timestamps } from '../../../common/utils/table';
import { publishersToExternalIdsTable } from './publishersToExternalIds.table' import { publishersToExternalIdsTable } from './publishersToExternalIds.table';
import { publishers_to_games } from './publishersToGames.table' import { publishers_to_games } from './publishersToGames.table';
export const publishersTable = pgTable('publishers', { export const publishersTable = pgTable('publishers', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid().primaryKey().defaultRandom(),
cuid: text('cuid') cuid: text()
.unique() .unique()
.$defaultFn(() => cuid2()), .$defaultFn(() => cuid2()),
name: text('name'), name: text(),
slug: text('slug'), slug: text(),
...timestamps, ...timestamps,
}) });
export type Publishers = InferSelectModel<typeof publishersTable> export type Publishers = InferSelectModel<typeof publishersTable>;
export const publishers_relations = relations(publishersTable, ({ many }) => ({ export const publishers_relations = relations(publishersTable, ({ many }) => ({
publishersToGames: many(publishers_to_games), publishersToGames: many(publishers_to_games),
publishersToExternalIds: many(publishersToExternalIdsTable), 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 { relations } from 'drizzle-orm';
import { externalIdsTable } from './externalIds.table' import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core';
import { publishersTable } from './publishers.table' import { externalIdsTable } from './externalIds.table';
import { relations } from 'drizzle-orm' import { publishersTable } from './publishers.table';
export const publishersToExternalIdsTable = pgTable( export const publishersToExternalIdsTable = pgTable(
'publishers_to_external_ids', 'publishers_to_external_ids',
{ {
publisherId: uuid('publisher_id') publisherId: uuid()
.notNull() .notNull()
.references(() => publishersTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }), .references(() => publishersTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
externalId: uuid('external_id') externalId: uuid()
.notNull() .notNull()
.references(() => externalIdsTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }), .references(() => externalIdsTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
}, },
@ -18,9 +18,9 @@ export const publishersToExternalIdsTable = pgTable(
publishersToExternalIdsPkey: primaryKey({ publishersToExternalIdsPkey: primaryKey({
columns: [table.publisherId, table.externalId], columns: [table.publisherId, table.externalId],
}), }),
} };
}, },
) );
export const publishersToExternalIdsRelations = relations(publishersToExternalIdsTable, ({ one }) => ({ export const publishersToExternalIdsRelations = relations(publishersToExternalIdsTable, ({ one }) => ({
publisher: one(publishersTable, { publisher: one(publishersTable, {
@ -31,4 +31,4 @@ export const publishersToExternalIdsRelations = relations(publishersToExternalId
fields: [publishersToExternalIdsTable.externalId], fields: [publishersToExternalIdsTable.externalId],
references: [externalIdsTable.id], references: [externalIdsTable.id],
}), }),
})) }));

View file

@ -1,15 +1,15 @@
import { relations } from 'drizzle-orm' import { relations } from 'drizzle-orm';
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core' import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core';
import { gamesTable } from '././games.table' import { gamesTable } from './games.table';
import { publishersTable } from './publishers.table' import { publishersTable } from './publishers.table';
export const publishers_to_games = pgTable( export const publishers_to_games = pgTable(
'publishers_to_games', 'publishers_to_games',
{ {
publisher_id: uuid('publisher_id') publisher_id: uuid()
.notNull() .notNull()
.references(() => publishersTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }), .references(() => publishersTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
game_id: uuid('game_id') game_id: uuid()
.notNull() .notNull()
.references(() => gamesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }), .references(() => gamesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
}, },
@ -18,9 +18,9 @@ export const publishers_to_games = pgTable(
publishersToGamesPkey: primaryKey({ publishersToGamesPkey: primaryKey({
columns: [table.publisher_id, table.game_id], columns: [table.publisher_id, table.game_id],
}), }),
} };
}, },
) );
export const publishers_to_games_relations = relations(publishers_to_games, ({ one }) => ({ export const publishers_to_games_relations = relations(publishers_to_games, ({ one }) => ({
publisher: one(publishersTable, { publisher: one(publishersTable, {
@ -31,4 +31,4 @@ export const publishers_to_games_relations = relations(publishers_to_games, ({ o
fields: [publishers_to_games.game_id], fields: [publishers_to_games.game_id],
references: [gamesTable.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 { 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'; import { usersTable } from './users.table';
export const sessionsTable = pgTable('sessions', { export const sessionsTable = pgTable('sessions', {
id: text('id').primaryKey(), id: cuid2().primaryKey(),
userId: uuid('user_id') userId: uuid()
.notNull() .notNull()
.references(() => usersTable.id), .references(() => usersTable.id, { onDelete: 'cascade' }),
expiresAt: timestamp('expires_at', { expiresAt: timestamp({
withTimezone: true, withTimezone: true,
mode: 'date', mode: 'date',
}).notNull(), }).notNull(),
ipCountry: text('ip_country'), ipCountry: text(),
ipAddress: text('ip_address'), ipAddress: text(),
twoFactorAuthEnabled: boolean('two_factor_auth_enabled').default(false), twoFactorAuthEnabled: boolean().default(false),
isTwoFactorAuthenticated: boolean('is_two_factor_authenticated').default(false), isTwoFactorAuthenticated: boolean().default(false),
}); });
export const sessionsRelations = relations(sessionsTable, ({ one }) => ({ export const sessionsRelations = relations(sessionsTable, ({ one }) => ({
user: one(usersTable, { user: one(usersTable, {
fields: [sessionsTable.userId], fields: [sessionsTable.userId],
references: [usersTable.id], references: [usersTable.id],
}) }),
})); }));
export type Sessions = InferSelectModel<typeof sessionsTable>; 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 { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm' import { type InferSelectModel, relations } from 'drizzle-orm';
import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core' import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { timestamps } from '../../common/utils/table' import { timestamps } from '../../../common/utils/table';
import { rolesTable } from './roles.table' import { rolesTable } from './roles.table';
import { usersTable } from './users.table' import { usersTable } from './users.table';
export const user_roles = pgTable('user_roles', { export const user_roles = pgTable('user_roles', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid().primaryKey().defaultRandom(),
cuid: text('cuid') cuid: text()
.unique() .unique()
.$defaultFn(() => cuid2()), .$defaultFn(() => cuid2()),
user_id: uuid('user_id') user_id: uuid()
.notNull() .notNull()
.references(() => usersTable.id, { onDelete: 'cascade' }), .references(() => usersTable.id, { onDelete: 'cascade' }),
role_id: uuid('role_id') role_id: uuid()
.notNull() .notNull()
.references(() => rolesTable.id, { onDelete: 'cascade' }), .references(() => rolesTable.id, { onDelete: 'cascade' }),
primary: boolean('primary').default(false), primary: boolean().default(false),
...timestamps, ...timestamps,
}) });
export const user_role_relations = relations(user_roles, ({ one }) => ({ export const user_role_relations = relations(user_roles, ({ one }) => ({
role: one(rolesTable, { role: one(rolesTable, {
@ -29,6 +29,6 @@ export const user_role_relations = relations(user_roles, ({ one }) => ({
fields: [user_roles.user_id], fields: [user_roles.user_id],
references: [usersTable.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 { type InferSelectModel, relations } from 'drizzle-orm';
import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core'; import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { createSelectSchema } from 'drizzle-zod'; import { createSelectSchema } from 'drizzle-zod';
import { timestamps } from '../../common/utils/table'; import { timestamps } from '../../../common/utils/table';
import { user_roles } from './userRoles.table'; import { user_roles } from './userRoles.table';
export const usersTable = pgTable('users', { export const usersTable = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid().primaryKey().defaultRandom(),
cuid: text('cuid') cuid: text()
.unique() .unique()
.$defaultFn(() => cuid2()), .$defaultFn(() => cuid2()),
username: text('username').unique(), username: text().unique(),
email: text('email').unique(), email: text().unique(),
first_name: text('first_name'), first_name: text(),
last_name: text('last_name'), last_name: text(),
verified: boolean('verified').default(false), verified: boolean().default(false),
receive_email: boolean('receive_email').default(false), receive_email: boolean().default(false),
email_verified: boolean('email_verified').default(false), email_verified: boolean().default(false),
picture: text('picture'), picture: text(),
mfa_enabled: boolean('mfa_enabled').notNull().default(false), mfa_enabled: boolean().notNull().default(false),
theme: text('theme').default('system'), theme: text().default('system'),
...timestamps, ...timestamps,
}); });

View file

@ -1,25 +1,25 @@
import { createId as cuid2 } from '@paralleldrive/cuid2' import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm' import { type InferSelectModel, relations } from 'drizzle-orm';
import { pgTable, text, uuid } from 'drizzle-orm/pg-core' import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { timestamps } from '../../common/utils/table' import { timestamps } from '../../../common/utils/table';
import { gamesTable } from '././games.table' import { gamesTable } from './games.table';
import { wishlistsTable } from './wishlists.table' import { wishlistsTable } from './wishlists.table';
export const wishlist_items = pgTable('wishlist_items', { export const wishlist_items = pgTable('wishlist_items', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid().primaryKey().defaultRandom(),
cuid: text('cuid') cuid: text()
.unique() .unique()
.$defaultFn(() => cuid2()), .$defaultFn(() => cuid2()),
wishlist_id: uuid('wishlist_id') wishlist_id: uuid()
.notNull() .notNull()
.references(() => wishlistsTable.id, { onDelete: 'cascade' }), .references(() => wishlistsTable.id, { onDelete: 'cascade' }),
game_id: uuid('game_id') game_id: uuid()
.notNull() .notNull()
.references(() => gamesTable.id, { onDelete: 'cascade' }), .references(() => gamesTable.id, { onDelete: 'cascade' }),
...timestamps, ...timestamps,
}) });
export type WishlistItemsTable = InferSelectModel<typeof wishlist_items> export type WishlistItemsTable = InferSelectModel<typeof wishlist_items>;
export const wishlist_item_relations = relations(wishlist_items, ({ one }) => ({ export const wishlist_item_relations = relations(wishlist_items, ({ one }) => ({
wishlist: one(wishlistsTable, { wishlist: one(wishlistsTable, {
@ -30,4 +30,4 @@ export const wishlist_item_relations = relations(wishlist_items, ({ one }) => ({
fields: [wishlist_items.game_id], fields: [wishlist_items.game_id],
references: [gamesTable.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 createApp from '$lib/server/api/common/create-app';
import configureOpenAPI from '$lib/server/api/configure-open-api'; import configureOpenAPI from '$lib/server/api/configure-open-api';
import { CollectionController } from '$lib/server/api/controllers/collection.controller'; import { CollectionController } from '$lib/server/api/controllers/collection.controller';
@ -34,7 +33,6 @@ const routes = app
.route('/mfa', container.resolve(MfaController).routes()) .route('/mfa', container.resolve(MfaController).routes())
.get('/', (c) => c.json({ message: 'Server is healthy' })); .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); 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 type { MiddlewareHandler } from 'hono';
import { createMiddleware } from 'hono/factory'; import { createMiddleware } from 'hono/factory';
import { parseCookies } from 'oslo/cookie';
import { verifyRequestOrigin } from 'oslo/request'; import { verifyRequestOrigin } from 'oslo/request';
import { container } from 'tsyringe'; import { container } from 'tsyringe';
import type { AppBindings } from '../common/types/hono'; import type { AppBindings } from '../common/types/hono';
// resolve dependencies from the container // resolve dependencies from the container
const { lucia } = container.resolve(LuciaService); const sessionService = container.resolve(SessionsService);
export const verifyOrigin: MiddlewareHandler<AppBindings> = createMiddleware(async (c, next) => { export const verifyOrigin: MiddlewareHandler<AppBindings> = createMiddleware(async (c, next) => {
if (c.req.method === 'GET') { 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) => { 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) { if (!sessionId) {
c.set('user', null); c.set('user', null);
c.set('session', null); c.set('session', null);
return next(); return next();
} }
const { session, user } = await lucia.validateSession(sessionId); const { session, user } = await sessionService.validateSessionToken(sessionId);
if (session?.fresh) { let sessionCookie: SessionCookie;
c.header('Set-Cookie', lucia.createSessionCookie(session.id).serialize(), { append: true }); if (session !== null) {
} sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt);
if (!session) { } else {
c.header('Set-Cookie', lucia.createBlankSessionCookie().serialize(), { append: true }); sessionCookie = createBlankSessionTokenCookie();
} }
setSessionCookie(c, sessionCookie);
c.set('session', session); c.set('session', session);
c.set('user', user); c.set('user', user);
return next(); return next();

View file

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

View file

@ -1,22 +1,24 @@
import { drizzle } from 'drizzle-orm/node-postgres' import { drizzle } from 'drizzle-orm/node-postgres';
import pg from 'pg' import pg from 'pg';
import { config } from '../common/config' import { config } from '../common/config';
import * as schema from '../databases/tables' import * as schema from '../databases/postgres/tables';
// create the connection // create the connection
export const pool = new pg.Pool({ export const pool = new pg.Pool({
user: config.DATABASE_USER, user: config.postgres.user,
password: config.DATABASE_PASSWORD, password: config.postgres.password,
host: config.DATABASE_HOST, host: config.postgres.host,
port: Number(config.DATABASE_PORT).valueOf(), port: Number(config.postgres.port).valueOf(),
database: config.DATABASE_DB, database: config.postgres.database,
ssl: config.DATABASE_HOST !== 'localhost', ssl: config.postgres.host !== 'localhost',
max: config.DB_MIGRATING || config.DB_SEEDING ? 1 : undefined, max: config.postgres.migrating || config.postgres.seeding ? 1 : undefined,
}) });
export const db = drizzle(pool, { export const db = drizzle({
client: pool,
casing: 'snake_case',
schema, 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 { takeFirstOrThrow } from '$lib/server/api/common/utils/repository';
import { DrizzleService } from '$lib/server/api/services/drizzle.service' import { DrizzleService } from '$lib/server/api/services/drizzle.service';
import { type InferInsertModel, eq } from 'drizzle-orm' import { type InferInsertModel, eq } from 'drizzle-orm';
import { inject, injectable } from 'tsyringe' import { inject, injectable } from 'tsyringe';
import { collections } from '../databases/tables' import { collections } from '../databases/postgres/tables';
export type CreateCollection = InferInsertModel<typeof collections> export type CreateCollection = InferInsertModel<typeof collections>;
export type UpdateCollection = Partial<CreateCollection> export type UpdateCollection = Partial<CreateCollection>;
@injectable() @injectable()
export class CollectionsRepository { export class CollectionsRepository {
constructor(@inject(DrizzleService) private readonly drizzle: DrizzleService) {} constructor(@inject(DrizzleService) private readonly drizzle: DrizzleService) {}
async findAll(db = this.drizzle.db) { async findAll(db = this.drizzle.db) {
return db.query.collections.findMany() return db.query.collections.findMany();
} }
async findOneById(id: string, db = this.drizzle.db) { async findOneById(id: string, db = this.drizzle.db) {
@ -22,7 +22,7 @@ export class CollectionsRepository {
cuid: true, cuid: true,
name: true, name: true,
}, },
}) });
} }
async findOneByCuid(cuid: string, db = this.drizzle.db) { async findOneByCuid(cuid: string, db = this.drizzle.db) {
@ -32,7 +32,7 @@ export class CollectionsRepository {
cuid: true, cuid: true,
name: true, name: true,
}, },
}) });
} }
async findOneByUserId(userId: string, db = this.drizzle.db) { async findOneByUserId(userId: string, db = this.drizzle.db) {
@ -42,7 +42,7 @@ export class CollectionsRepository {
cuid: true, cuid: true,
name: true, name: true,
}, },
}) });
} }
async findAllByUserId(userId: string, db = this.drizzle.db) { async findAllByUserId(userId: string, db = this.drizzle.db) {
@ -53,7 +53,7 @@ export class CollectionsRepository {
name: true, name: true,
createdAt: true, createdAt: true,
}, },
}) });
} }
async findAllByUserIdWithDetails(userId: string, db = this.drizzle.db) { async findAllByUserIdWithDetails(userId: string, db = this.drizzle.db) {
@ -70,14 +70,14 @@ export class CollectionsRepository {
}, },
}, },
}, },
}) });
} }
async create(data: CreateCollection, db = this.drizzle.db) { 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) { 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 'reflect-metadata';
import { CredentialsType, credentialsTable } from '$lib/server/api/databases/tables/credentials.table' import { CredentialsType, credentialsTable } from '$lib/server/api/databases/postgres/tables/credentials.table';
import { DrizzleService } from '$lib/server/api/services/drizzle.service' import { DrizzleService } from '$lib/server/api/services/drizzle.service';
import { type InferInsertModel, and, eq } from 'drizzle-orm' import { type InferInsertModel, and, eq } from 'drizzle-orm';
import { inject, injectable } from 'tsyringe' import { inject, injectable } from 'tsyringe';
import { takeFirstOrThrow } from '../common/utils/repository' import { takeFirstOrThrow } from '../common/utils/repository';
export type CreateCredentials = InferInsertModel<typeof credentialsTable> export type CreateCredentials = InferInsertModel<typeof credentialsTable>;
export type UpdateCredentials = Partial<CreateCredentials> export type UpdateCredentials = Partial<CreateCredentials>;
export type DeleteCredentials = Pick<CreateCredentials, 'id'> export type DeleteCredentials = Pick<CreateCredentials, 'id'>;
@injectable() @injectable()
export class CredentialsRepository { export class CredentialsRepository {
@ -16,56 +16,56 @@ export class CredentialsRepository {
async findOneByUserId(userId: string, db = this.drizzle.db) { async findOneByUserId(userId: string, db = this.drizzle.db) {
return db.query.credentialsTable.findFirst({ return db.query.credentialsTable.findFirst({
where: eq(credentialsTable.user_id, userId), where: eq(credentialsTable.user_id, userId),
}) });
} }
async findOneByUserIdAndType(userId: string, type: CredentialsType, db = this.drizzle.db) { async findOneByUserIdAndType(userId: string, type: CredentialsType, db = this.drizzle.db) {
return db.query.credentialsTable.findFirst({ return db.query.credentialsTable.findFirst({
where: and(eq(credentialsTable.user_id, userId), eq(credentialsTable.type, type)), where: and(eq(credentialsTable.user_id, userId), eq(credentialsTable.type, type)),
}) });
} }
async findPasswordCredentialsByUserId(userId: string, db = this.drizzle.db) { async findPasswordCredentialsByUserId(userId: string, db = this.drizzle.db) {
return db.query.credentialsTable.findFirst({ return db.query.credentialsTable.findFirst({
where: and(eq(credentialsTable.user_id, userId), eq(credentialsTable.type, CredentialsType.PASSWORD)), where: and(eq(credentialsTable.user_id, userId), eq(credentialsTable.type, CredentialsType.PASSWORD)),
}) });
} }
async findTOTPCredentialsByUserId(userId: string, db = this.drizzle.db) { async findTOTPCredentialsByUserId(userId: string, db = this.drizzle.db) {
return db.query.credentialsTable.findFirst({ return db.query.credentialsTable.findFirst({
where: and(eq(credentialsTable.user_id, userId), eq(credentialsTable.type, CredentialsType.TOTP)), where: and(eq(credentialsTable.user_id, userId), eq(credentialsTable.type, CredentialsType.TOTP)),
}) });
} }
async findOneById(id: string, db = this.drizzle.db) { async findOneById(id: string, db = this.drizzle.db) {
return db.query.credentialsTable.findFirst({ return db.query.credentialsTable.findFirst({
where: eq(credentialsTable.id, id), where: eq(credentialsTable.id, id),
}) });
} }
async findOneByIdOrThrow(id: string, db = this.drizzle.db) { async findOneByIdOrThrow(id: string, db = this.drizzle.db) {
const credentials = await this.findOneById(id, db) const credentials = await this.findOneById(id, db);
if (!credentials) throw Error('Credentials not found') if (!credentials) throw Error('Credentials not found');
return credentials return credentials;
} }
async create(data: CreateCredentials, db = this.drizzle.db) { 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) { 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) { 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) { 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) { 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 { type InferInsertModel, and, eq } from 'drizzle-orm';
import { inject, injectable } from 'tsyringe' import { inject, injectable } from 'tsyringe';
import { takeFirstOrThrow } from '../common/utils/repository' import { takeFirstOrThrow } from '../common/utils/repository';
import { federatedIdentityTable } from '../databases/tables' import { federatedIdentityTable } from '../databases/postgres/tables';
import { DrizzleService } from '../services/drizzle.service' import { DrizzleService } from '../services/drizzle.service';
export type CreateFederatedIdentity = InferInsertModel<typeof federatedIdentityTable> export type CreateFederatedIdentity = InferInsertModel<typeof federatedIdentityTable>;
@injectable() @injectable()
export class FederatedIdentityRepository { export class FederatedIdentityRepository {
@ -13,16 +13,16 @@ export class FederatedIdentityRepository {
async findOneByUserIdAndProvider(userId: string, provider: string) { async findOneByUserIdAndProvider(userId: string, provider: string) {
return this.drizzle.db.query.federatedIdentityTable.findFirst({ return this.drizzle.db.query.federatedIdentityTable.findFirst({
where: and(eq(federatedIdentityTable.user_id, userId), eq(federatedIdentityTable.identity_provider, provider)), where: and(eq(federatedIdentityTable.user_id, userId), eq(federatedIdentityTable.identity_provider, provider)),
}) });
} }
async findOneByFederatedUserIdAndProvider(federatedUserId: string, provider: string) { async findOneByFederatedUserIdAndProvider(federatedUserId: string, provider: string) {
return this.drizzle.db.query.federatedIdentityTable.findFirst({ return this.drizzle.db.query.federatedIdentityTable.findFirst({
where: and(eq(federatedIdentityTable.federated_user_id, federatedUserId), eq(federatedIdentityTable.identity_provider, provider)), where: and(eq(federatedIdentityTable.federated_user_id, federatedUserId), eq(federatedIdentityTable.identity_provider, provider)),
}) });
} }
async create(data: CreateFederatedIdentity, db = this.drizzle.db) { 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 'reflect-metadata';
import { takeFirstOrThrow } from '$lib/server/api/common/utils/repository' import { takeFirstOrThrow } from '$lib/server/api/common/utils/repository';
import { DrizzleService } from '$lib/server/api/services/drizzle.service' import { DrizzleService } from '$lib/server/api/services/drizzle.service';
import { type InferInsertModel, eq } from 'drizzle-orm' import { type InferInsertModel, eq } from 'drizzle-orm';
import { inject, injectable } from 'tsyringe' import { inject, injectable } from 'tsyringe';
import { recoveryCodesTable } from '../databases/tables' import { recoveryCodesTable } from '../databases/postgres/tables';
export type CreateRecoveryCodes = InferInsertModel<typeof recoveryCodesTable> export type CreateRecoveryCodes = InferInsertModel<typeof recoveryCodesTable>;
@injectable() @injectable()
export class RecoveryCodesRepository { export class RecoveryCodesRepository {
constructor(@inject(DrizzleService) private readonly drizzle: DrizzleService) {} constructor(@inject(DrizzleService) private readonly drizzle: DrizzleService) {}
async create(data: CreateRecoveryCodes, db = this.drizzle.db) { 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) { async findAllByUserId(userId: string, db = this.drizzle.db) {
return db.query.recoveryCodesTable.findMany({ return db.query.recoveryCodesTable.findMany({
where: eq(recoveryCodesTable.userId, userId), where: eq(recoveryCodesTable.userId, userId),
}) });
} }
async deleteAllByUserId(userId: string, db = this.drizzle.db) { 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 { DrizzleService } from '$lib/server/api/services/drizzle.service';
import { type InferInsertModel, eq } from 'drizzle-orm' import { type InferInsertModel, eq } from 'drizzle-orm';
import { inject, injectable } from 'tsyringe' import { inject, injectable } from 'tsyringe';
import { takeFirstOrThrow } from '../common/utils/repository' import { takeFirstOrThrow } from '../common/utils/repository';
import { rolesTable } from '../databases/tables' import { rolesTable } from '../databases/postgres/tables';
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* Repository */ /* 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. In our case the method 'trxHost' is used to set the transaction context.
*/ */
export type CreateRole = InferInsertModel<typeof rolesTable> export type CreateRole = InferInsertModel<typeof rolesTable>;
export type UpdateRole = Partial<CreateRole> export type UpdateRole = Partial<CreateRole>;
@injectable() @injectable()
export class RolesRepository { export class RolesRepository {
@ -30,40 +30,40 @@ export class RolesRepository {
async findOneById(id: string, db = this.drizzle.db) { async findOneById(id: string, db = this.drizzle.db) {
return db.query.rolesTable.findFirst({ return db.query.rolesTable.findFirst({
where: eq(rolesTable.id, id), where: eq(rolesTable.id, id),
}) });
} }
async findOneByIdOrThrow(id: string, db = this.drizzle.db) { async findOneByIdOrThrow(id: string, db = this.drizzle.db) {
const role = await this.findOneById(id, db) const role = await this.findOneById(id, db);
if (!role) throw Error('Role not found') if (!role) throw Error('Role not found');
return role return role;
} }
async findAll(db = this.drizzle.db) { async findAll(db = this.drizzle.db) {
return db.query.rolesTable.findMany() return db.query.rolesTable.findMany();
} }
async findOneByName(name: string, db = this.drizzle.db) { async findOneByName(name: string, db = this.drizzle.db) {
return db.query.rolesTable.findFirst({ return db.query.rolesTable.findFirst({
where: eq(rolesTable.name, name), where: eq(rolesTable.name, name),
}) });
} }
async findOneByNameOrThrow(name: string, db = this.drizzle.db) { async findOneByNameOrThrow(name: string, db = this.drizzle.db) {
const role = await this.findOneByName(name, db) const role = await this.findOneByName(name, db);
if (!role) throw Error('Role not found') if (!role) throw Error('Role not found');
return role return role;
} }
async create(data: CreateRole, db = this.drizzle.db) { 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) { 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) { 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