Formatting files, upgrading Drizzle, using snake case in Drizzle, update tables, fix unique ID with new Drizzle.

This commit is contained in:
Bradley Shellnut 2024-11-06 09:49:18 -08:00
parent 05b74e4c59
commit 3993596986
120 changed files with 8343 additions and 2338 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.48.0", "@playwright/test": "^1.48.2",
"@sveltejs/adapter-auto": "^3.2.5", "@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/enhanced-img": "^0.3.9", "@sveltejs/enhanced-img": "^0.3.10",
"@sveltejs/kit": "^2.7.1", "@sveltejs/kit": "^2.7.5",
"@sveltejs/vite-plugin-svelte": "4.0.0-next.7", "@sveltejs/vite-plugin-svelte": "4.0.0-next.7",
"@types/cookie": "^0.6.0", "@types/cookie": "^0.6.0",
"@types/node": "^20.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.1",
"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.3", "typescript": "^5.6.3",
"vite": "^5.4.9", "vite": "^5.4.10",
"vitest": "^1.6.0", "vitest": "^1.6.0",
"zod": "^3.23.8" "zod": "^3.23.8"
}, },
@ -92,27 +92,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.154", "@scalar/hono-api-reference": "^0.5.158",
"@sveltejs/adapter-node": "^5.2.7", "@sveltejs/adapter-node": "^5.2.9",
"@sveltejs/adapter-vercel": "^5.4.5", "@sveltejs/adapter-vercel": "^5.4.6",
"@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.20.0", "bullmq": "^5.24.0",
"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.0",
"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.4", "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.3.0", "hono-zod-openapi": "^0.3.1",
"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",
@ -122,21 +122,21 @@
"mode-watcher": "^0.4.1", "mode-watcher": "^0.4.1",
"open-props": "^1.7.7", "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.2.3", "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.4", "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

@ -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

@ -0,0 +1,34 @@
import { config } from '$lib/server/api/common/config';
import env from '$lib/server/api/common/env';
export function createSessionTokenCookie(token: string, expiresAt: Date) {
return {
name: 'session',
value: token,
attributes: {
path: '/',
maxAge: 60 * 60 * 24 * 30,
domain: env.DOMAIN,
sameSite: 'lax',
secure: config.isProduction,
httpOnly: true,
expires: expiresAt,
},
};
}
export function createBlankSessionTokenCookie() {
return {
name: 'session',
value: '',
attributes: {
path: '/',
maxAge: 0,
domain: env.DOMAIN,
sameSite: 'lax',
secure: config.isProduction,
httpOnly: true,
expires: new Date(0),
},
};
}

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,11 +1,11 @@
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 cuidParamsSchema from '$lib/server/api/common/openapi/cuidParamsSchema'; import cuidParamsSchema from '$lib/server/api/common/openapi/cuidParamsSchema';
import { selectCollectionSchema } from '$lib/server/api/databases/tables';
import { z } from '@hono/zod-openapi'; import { z } from '@hono/zod-openapi';
import { IdParamsSchema } from 'stoker/openapi/schemas'; import { IdParamsSchema } from 'stoker/openapi/schemas';
import { createErrorSchema } from 'stoker/openapi/schemas'; import { createErrorSchema } from 'stoker/openapi/schemas';
import { taggedAuthRoute } from '../common/openapi/create-auth-route'; import { taggedAuthRoute } from '../common/openapi/create-auth-route';
import { selectCollectionSchema } from '../databases/postgres/tables';
const tag = 'Collection'; const tag = 'Collection';

View file

@ -1,5 +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 { deleteSessionTokenCookie } 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';
@ -7,7 +8,7 @@ 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';
@ -20,7 +21,7 @@ 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();
} }
@ -78,18 +79,9 @@ export class IamController extends Controller {
} }
try { try {
await this.iamService.updatePassword(user.id, { password, confirm_password }); await this.iamService.updatePassword(user.id, { password, confirm_password });
await this.luciaService.lucia.invalidateUserSessions(user.id); await this.sessionsService.invalidateSession(user.id);
await this.loginRequestService.createUserSession(user.id, c.req, undefined); await this.loginRequestService.createUserSession(user.id, c.req, undefined);
const sessionCookie = this.luciaService.lucia.createBlankSessionCookie(); deleteSessionTokenCookie(c);
setCookie(c, sessionCookie.name, sessionCookie.value, {
path: sessionCookie.attributes.path,
maxAge: sessionCookie.attributes.maxAge,
domain: sessionCookie.attributes.domain,
sameSite: sessionCookie.attributes.sameSite as any,
secure: sessionCookie.attributes.secure,
httpOnly: sessionCookie.attributes.httpOnly,
expires: sessionCookie.attributes.expires,
});
return c.json({ status: 'success' }); return c.json({ status: 'success' });
} catch (error) { } catch (error) {
console.error('Error updating password', error); console.error('Error updating password', error);
@ -116,16 +108,7 @@ export class IamController extends Controller {
.post('/logout', requireAuth, openApi(logout), async (c) => { .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(); deleteSessionTokenCookie(c);
setCookie(c, sessionCookie.name, sessionCookie.value, {
path: sessionCookie.attributes.path,
maxAge: sessionCookie.attributes.maxAge,
domain: sessionCookie.attributes.domain,
sameSite: sessionCookie.attributes.sameSite as any,
secure: sessionCookie.attributes.secure,
httpOnly: sessionCookie.attributes.httpOnly,
expires: sessionCookie.attributes.expires,
});
return c.json({ status: 'success' }); return c.json({ status: 'success' });
}); });
} }

View file

@ -1,6 +1,6 @@
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 { createErrorSchema } from 'stoker/openapi/schemas'; import { createErrorSchema } from 'stoker/openapi/schemas';
import { taggedAuthRoute } from '../common/openapi/create-auth-route'; import { taggedAuthRoute } from '../common/openapi/create-auth-route';

View file

@ -1,44 +1,50 @@
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 { signinUsernameDto } from '$lib/server/api/dtos/signin-username.dto';
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 { setCookie } from 'hono/cookie'
import { TimeSpan } from 'oslo'
import { openApi } from 'hono-zod-openapi'; import { openApi } from 'hono-zod-openapi';
import { inject, injectable } from 'tsyringe' import { setCookie } from 'hono/cookie';
import { limiter } from '../middleware/rate-limiter.middleware' import { TimeSpan } from 'oslo';
import { LoginRequestsService } from '../services/loginrequest.service' import { inject, injectable } from 'tsyringe';
import { signinUsername } from './login.routes' import { limiter } from '../middleware/rate-limiter.middleware';
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('/', openApi(signinUsername), 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 = this.luciaService.lucia.createSessionCookie(session.id);
? sessionCookie.attributes.maxAge console.log('set cookie', sessionCookie);
: new TimeSpan(2, 'w').seconds(), setCookie(c, sessionCookie.name, sessionCookie.value, {
domain: sessionCookie.attributes.domain, path: sessionCookie.attributes.path,
sameSite: sessionCookie.attributes.sameSite as any, maxAge:
secure: sessionCookie.attributes.secure, sessionCookie?.attributes?.maxAge && sessionCookie?.attributes?.maxAge < new TimeSpan(365, 'd').seconds()
httpOnly: sessionCookie.attributes.httpOnly, ? sessionCookie.attributes.maxAge
expires: sessionCookie.attributes.expires, : new TimeSpan(2, 'w').seconds(),
}) 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

@ -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,53 +1,56 @@
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 { createSessionTokenCookie } 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,
req.);
const sessionCookie = createSessionTokenCookie(session.id, new Date(new TimeSpan(2, 'w').milliseconds()));
setCookie(c, sessionCookie.name, sessionCookie.value, { setCookie(c, sessionCookie.name, sessionCookie.value, {
path: sessionCookie.attributes.path, path: sessionCookie.attributes.path,
@ -60,37 +63,37 @@ export class OAuthController 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' });
} 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,12 +103,12 @@ 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 session = await this.luciaService.lucia.createSession(userId, {}) const session = await this.luciaService.lucia.createSession(userId, {});
const sessionCookie = this.luciaService.lucia.createSessionCookie(session.id) const sessionCookie = this.luciaService.lucia.createSessionCookie(session.id);
setCookie(c, sessionCookie.name, sessionCookie.value, { setCookie(c, sessionCookie.name, sessionCookie.value, {
path: sessionCookie.attributes.path, path: sessionCookie.attributes.path,
@ -118,33 +121,33 @@ export class OAuthController 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' });
} 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,33 @@
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();
}
if (!env.DB_MIGRATING) {
throw new Error('You must set DB_MIGRATING to "true" when running migrations.');
}
await migrate(db, { migrationsFolder: config.out });
console.log('Migrations complete');
} catch (e) {
console.error(e);
}
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,19 @@
import 'reflect-metadata' 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 +45,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 { 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,6 +1,6 @@
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import type { db } from '../../packages/drizzle'; import type { db } from '../../../packages/drizzle';
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';

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

@ -2,19 +2,19 @@ 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 { createSelectSchema } from 'drizzle-zod'; import { createSelectSchema } from 'drizzle-zod';
import { timestamps } from '../../common/utils/table'; import { timestamps } from '../../../common/utils/table';
import { collection_items } from './collectionItems.table'; import { collection_items } from './collectionItems.table';
import { usersTable } from './users.table'; import { usersTable } from './users.table';
export const collections = pgTable('collections', { export const collections = pgTable('collections', {
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' }),
name: text('name').notNull().default('My Collection'), name: text().notNull().default('My Collection'),
...timestamps, ...timestamps,
}); });

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,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,5 @@
import { LuciaService } from '$lib/server/api/services/lucia.service'; import { createSessionTokenCookie } 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 { verifyRequestOrigin } from 'oslo/request'; import { verifyRequestOrigin } from 'oslo/request';
@ -6,7 +7,7 @@ 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') {
@ -30,7 +31,7 @@ export const validateAuthSession: MiddlewareHandler<AppBindings> = createMiddlew
const { session, user } = await lucia.validateSession(sessionId); const { session, user } = await lucia.validateSession(sessionId);
if (session?.fresh) { if (session?.fresh) {
c.header('Set-Cookie', lucia.createSessionCookie(session.id).serialize(), { append: true }); c.header('Set-Cookie', createSessionTokenCookie(session.id).serialize(), { append: true });
} }
if (!session) { if (!session) {
c.header('Set-Cookie', lucia.createBlankSessionCookie().serialize(), { append: true }); c.header('Set-Cookie', lucia.createBlankSessionCookie().serialize(), { append: true });

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 { Pool } 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 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));
}
}

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 { user_roles } from '../databases/tables' import { user_roles } 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 CreateUserRole = InferInsertModel<typeof user_roles> export type CreateUserRole = InferInsertModel<typeof user_roles>;
export type UpdateUserRole = Partial<CreateUserRole> export type UpdateUserRole = Partial<CreateUserRole>;
@injectable() @injectable()
export class UserRolesRepository { export class UserRolesRepository {
@ -30,26 +30,26 @@ export class UserRolesRepository {
async findOneById(id: string, db = this.drizzle.db) { async findOneById(id: string, db = this.drizzle.db) {
return db.query.user_roles.findFirst({ return db.query.user_roles.findFirst({
where: eq(user_roles.id, id), where: eq(user_roles.id, id),
}) });
} }
async findOneByIdOrThrow(id: string, db = this.drizzle.db) { async findOneByIdOrThrow(id: string, db = this.drizzle.db) {
const userRole = await this.findOneById(id, db) const userRole = await this.findOneById(id, db);
if (!userRole) throw Error('User not found') if (!userRole) throw Error('User not found');
return userRole return userRole;
} }
async findAllByUserId(userId: string, db = this.drizzle.db) { async findAllByUserId(userId: string, db = this.drizzle.db) {
return db.query.user_roles.findMany({ return db.query.user_roles.findMany({
where: eq(user_roles.user_id, userId), where: eq(user_roles.user_id, userId),
}) });
} }
async create(data: CreateUserRole, db = this.drizzle.db) { async create(data: CreateUserRole, db = this.drizzle.db) {
return db.insert(user_roles).values(data).returning().then(takeFirstOrThrow) return db.insert(user_roles).values(data).returning().then(takeFirstOrThrow);
} }
async delete(id: string, db = this.drizzle.db) { async delete(id: string, db = this.drizzle.db) {
return db.delete(user_roles).where(eq(user_roles.id, id)).returning().then(takeFirstOrThrow) return db.delete(user_roles).where(eq(user_roles.id, id)).returning().then(takeFirstOrThrow);
} }
} }

View file

@ -1,4 +1,4 @@
import { usersTable } from '$lib/server/api/databases/tables/users.table'; import { usersTable } from '$lib/server/api/databases/postgres/tables/users.table';
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';

View file

@ -1,18 +1,18 @@
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 { wishlistsTable } from '../databases/tables' import { wishlistsTable } from '../databases/postgres/tables';
export type CreateWishlist = InferInsertModel<typeof wishlistsTable> export type CreateWishlist = InferInsertModel<typeof wishlistsTable>;
export type UpdateWishlist = Partial<CreateWishlist> export type UpdateWishlist = Partial<CreateWishlist>;
@injectable() @injectable()
export class WishlistsRepository { export class WishlistsRepository {
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.wishlistsTable.findMany() return db.query.wishlistsTable.findMany();
} }
async findOneById(id: string, db = this.drizzle.db) { async findOneById(id: string, db = this.drizzle.db) {
@ -22,7 +22,7 @@ export class WishlistsRepository {
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 WishlistsRepository {
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 WishlistsRepository {
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,14 +53,14 @@ export class WishlistsRepository {
name: true, name: true,
createdAt: true, createdAt: true,
}, },
}) });
} }
async create(data: CreateWishlist, db = this.drizzle.db) { async create(data: CreateWishlist, db = this.drizzle.db) {
return db.insert(wishlistsTable).values(data).returning().then(takeFirstOrThrow) return db.insert(wishlistsTable).values(data).returning().then(takeFirstOrThrow);
} }
async update(id: string, data: UpdateWishlist, db = this.drizzle.db) { async update(id: string, data: UpdateWishlist, db = this.drizzle.db) {
return db.update(wishlistsTable).set(data).where(eq(wishlistsTable.id, id)).returning().then(takeFirstOrThrow) return db.update(wishlistsTable).set(data).where(eq(wishlistsTable.id, id)).returning().then(takeFirstOrThrow);
} }
} }

View file

@ -3,7 +3,7 @@ import { type NodePgDatabase, drizzle } from 'drizzle-orm/node-postgres';
import pg from 'pg'; import pg from 'pg';
import { type Disposable, injectable } from 'tsyringe'; import { type Disposable, injectable } from 'tsyringe';
import { config } from '../common/config'; import { config } from '../common/config';
import * as schema from '../databases/tables'; import * as schema from '../databases/postgres/tables';
@injectable() @injectable()
export class DrizzleService implements Disposable { export class DrizzleService implements Disposable {

View file

@ -1,11 +1,11 @@
import { CredentialsType } from '$lib/server/api/databases/tables' import type { ChangePasswordDto } from '$lib/server/api/dtos/change-password.dto';
import type { ChangePasswordDto } from '$lib/server/api/dtos/change-password.dto' import type { UpdateEmailDto } from '$lib/server/api/dtos/update-email.dto';
import type { UpdateEmailDto } from '$lib/server/api/dtos/update-email.dto' import type { UpdateProfileDto } from '$lib/server/api/dtos/update-profile.dto';
import type { UpdateProfileDto } from '$lib/server/api/dtos/update-profile.dto' import type { VerifyPasswordDto } from '$lib/server/api/dtos/verify-password.dto';
import type { VerifyPasswordDto } from '$lib/server/api/dtos/verify-password.dto' import { SessionsService } from '$lib/server/api/services/sessions.service';
import { LuciaService } from '$lib/server/api/services/lucia.service' import { UsersService } from '$lib/server/api/services/users.service';
import { UsersService } from '$lib/server/api/services/users.service' import { inject, injectable } from 'tsyringe';
import { inject, injectable } from 'tsyringe' import { CredentialsType } from '../databases/postgres/tables';
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* Service */ /* Service */
@ -27,60 +27,60 @@ simple as possible. This makes the service easier to read, test and understand.
@injectable() @injectable()
export class IamService { export class IamService {
constructor( constructor(
@inject(LuciaService) private luciaService: LuciaService, @inject(SessionsService) private luciaService: SessionsService,
@inject(UsersService) private readonly usersService: UsersService, @inject(UsersService) private readonly usersService: UsersService,
) {} ) {}
async logout(sessionId: string) { async logout(sessionId: string) {
return this.luciaService.lucia.invalidateSession(sessionId) return this.luciaService.lucia.invalidateSession(sessionId);
} }
async updateProfile(userId: string, data: UpdateProfileDto) { async updateProfile(userId: string, data: UpdateProfileDto) {
const user = await this.usersService.findOneById(userId) const user = await this.usersService.findOneById(userId);
if (!user) { if (!user) {
return { return {
error: 'User not found', error: 'User not found',
} };
} }
const existingUserForNewUsername = await this.usersService.findOneByUsername(data.username) const existingUserForNewUsername = await this.usersService.findOneByUsername(data.username);
if (existingUserForNewUsername && existingUserForNewUsername.id !== user.id) { if (existingUserForNewUsername && existingUserForNewUsername.id !== user.id) {
return { return {
error: 'Username already in use', error: 'Username already in use',
} };
} }
return this.usersService.updateUser(user.id, { return this.usersService.updateUser(user.id, {
first_name: data.firstName, first_name: data.firstName,
last_name: data.lastName, last_name: data.lastName,
username: data.username !== user.username ? data.username : user.username, username: data.username !== user.username ? data.username : user.username,
}) });
} }
async updateEmail(userId: string, data: UpdateEmailDto) { async updateEmail(userId: string, data: UpdateEmailDto) {
const { email } = data const { email } = data;
const existingUserEmail = await this.usersService.findOneByEmail(email) const existingUserEmail = await this.usersService.findOneByEmail(email);
if (existingUserEmail && existingUserEmail.id !== userId) { if (existingUserEmail && existingUserEmail.id !== userId) {
return null return null;
} }
return this.usersService.updateUser(userId, { return this.usersService.updateUser(userId, {
email, email,
}) });
} }
async updatePassword(userId: string, data: ChangePasswordDto) { async updatePassword(userId: string, data: ChangePasswordDto) {
const { password } = data const { password } = data;
await this.usersService.updatePassword(userId, password) await this.usersService.updatePassword(userId, password);
} }
async verifyPassword(userId: string, data: VerifyPasswordDto) { async verifyPassword(userId: string, data: VerifyPasswordDto) {
const user = await this.usersService.findOneById(userId) const user = await this.usersService.findOneById(userId);
if (!user) { if (!user) {
return null return null;
} }
const { password } = data const { password } = data;
return this.usersService.verifyPassword(userId, { password }) return this.usersService.verifyPassword(userId, { password });
} }
} }

View file

@ -1,19 +1,19 @@
import type { SigninUsernameDto } from '$lib/server/api/dtos/signin-username.dto' import type { SigninUsernameDto } from '$lib/server/api/dtos/signin-username.dto';
import { LuciaService } from '$lib/server/api/services/lucia.service' import { SessionsService } from '$lib/server/api/services/sessions.service';
import type { HonoRequest } from 'hono' import type { HonoRequest } from 'hono';
import { inject, injectable } from 'tsyringe' import { inject, injectable } from 'tsyringe';
import { BadRequest } from '../common/exceptions' import { BadRequest } from '../common/exceptions';
import type { Credentials } from '../databases/tables' import type { Credentials } from '../databases/postgres/tables';
import { DatabaseProvider } from '../providers/database.provider' import { DatabaseProvider } from '../providers/database.provider';
import { CredentialsRepository } from '../repositories/credentials.repository' import { CredentialsRepository } from '../repositories/credentials.repository';
import { UsersRepository } from '../repositories/users.repository' import { UsersRepository } from '../repositories/users.repository';
import { MailerService } from './mailer.service' import { MailerService } from './mailer.service';
import { TokensService } from './tokens.service' import { TokensService } from './tokens.service';
@injectable() @injectable()
export class LoginRequestsService { export class LoginRequestsService {
constructor( constructor(
@inject(LuciaService) private luciaService: LuciaService, @inject(SessionsService) private sessionsService: SessionsService,
@inject(DatabaseProvider) private readonly db: DatabaseProvider, @inject(DatabaseProvider) private readonly db: DatabaseProvider,
@inject(TokensService) private readonly tokensService: TokensService, @inject(TokensService) private readonly tokensService: TokensService,
@inject(MailerService) private readonly mailerService: MailerService, @inject(MailerService) private readonly mailerService: MailerService,
@ -34,46 +34,48 @@ export class LoginRequestsService {
// } // }
async verify(data: SigninUsernameDto, req: HonoRequest) { async verify(data: SigninUsernameDto, req: HonoRequest) {
const requestIpAddress = req.header('x-real-ip') const requestIpAddress = req.header('x-real-ip');
const requestIpCountry = req.header('x-vercel-ip-country') const requestIpCountry = req.header('x-vercel-ip-country');
const existingUser = await this.usersRepository.findOneByUsername(data.username) const existingUser = await this.usersRepository.findOneByUsername(data.username);
if (!existingUser) { if (!existingUser) {
throw BadRequest('User not found') throw BadRequest('User not found');
} }
const credential = await this.credentialsRepository.findPasswordCredentialsByUserId(existingUser.id) const credential = await this.credentialsRepository.findPasswordCredentialsByUserId(existingUser.id);
if (!credential) { if (!credential) {
throw BadRequest('Invalid credentials') throw BadRequest('Invalid credentials');
} }
if (!(await this.tokensService.verifyHashedToken(credential.secret_data, data.password))) { if (!(await this.tokensService.verifyHashedToken(credential.secret_data, data.password))) {
throw BadRequest('Invalid credentials') throw BadRequest('Invalid credentials');
} }
const totpCredentials = await this.credentialsRepository.findTOTPCredentialsByUserId(existingUser.id) const totpCredentials = await this.credentialsRepository.findTOTPCredentialsByUserId(existingUser.id);
return await this.createUserSession(existingUser.id, req, totpCredentials) return await this.createUserSession(existingUser.id, req, totpCredentials);
} }
async createUserSession(existingUserId: string, req: HonoRequest, totpCredentials: Credentials | undefined) { async createUserSession(existingUserId: string, req: HonoRequest, totpCredentials: Credentials | undefined) {
const requestIpAddress = req.header('x-real-ip') const requestIpAddress = req.header('x-real-ip');
const requestIpCountry = req.header('x-vercel-ip-country') const requestIpCountry = req.header('x-vercel-ip-country');
return this.luciaService.lucia.createSession(existingUserId, { return this.sessionsService.createSession(
ip_country: requestIpCountry || 'unknown', this.sessionsService.generateSessionToken(),
ip_address: requestIpAddress || 'unknown', existingUserId,
twoFactorAuthEnabled: !!totpCredentials && totpCredentials?.secret_data !== null && totpCredentials?.secret_data !== '', requestIpCountry || 'unknown',
isTwoFactorAuthenticated: false, requestIpAddress || 'unknown',
}) !!totpCredentials && totpCredentials?.secret_data !== null && totpCredentials?.secret_data !== '',
false,
);
} }
// Create a new user and send a welcome email - or other onboarding process // Create a new user and send a welcome email - or other onboarding process
private async handleNewUserRegistration(email: string) { private async handleNewUserRegistration(email: string) {
const newUser = await this.usersRepository.create({ email, verified: true }) const newUser = await this.usersRepository.create({ email, verified: true });
this.mailerService.sendWelcome({ to: email, props: null }) this.mailerService.sendWelcome({ to: email, props: null });
// TODO: add whatever onboarding process or extra data you need here // TODO: add whatever onboarding process or extra data you need here
return newUser return newUser;
} }
// Fetch a valid request from the database, verify the token and burn the request if it is valid // Fetch a valid request from the database, verify the token and burn the request if it is valid

View file

@ -1,45 +0,0 @@
import { config } from '$lib/server/api/common/config'
import { DrizzleService } from '$lib/server/api/services/drizzle.service'
import { DrizzlePostgreSQLAdapter } from '@lucia-auth/adapter-drizzle'
import { Lucia, TimeSpan } from 'lucia'
import { inject, injectable } from 'tsyringe'
@injectable()
export class LuciaService {
readonly lucia: Lucia
constructor(@inject(DrizzleService) private readonly drizzle: DrizzleService) {
const adapter = new DrizzlePostgreSQLAdapter(this.drizzle.db, this.drizzle.schema.sessionsTable, this.drizzle.schema.usersTable)
this.lucia = new Lucia(adapter, {
sessionExpiresIn: new TimeSpan(2, 'w'), // 2 weeks
sessionCookie: {
name: 'session',
expires: false, // session cookies have very long lifespan (2 years)
attributes: {
// set to `true` when using HTTPS
secure: config.isProduction,
sameSite: 'strict',
domain: config.domain,
},
},
getSessionAttributes: (attributes) => {
return {
ipCountry: attributes.ip_country,
ipAddress: attributes.ip_address,
isTwoFactorAuthEnabled: attributes.twoFactorAuthEnabled,
isTwoFactorAuthenticated: attributes.isTwoFactorAuthenticated,
}
},
getUserAttributes: (attributes) => {
return {
// ...attributes,
username: attributes.username,
email: attributes.email,
firstName: attributes.first_name,
lastName: attributes.last_name,
mfa_enabled: attributes.mfa_enabled,
theme: attributes.theme,
}
},
})
}
}

View file

@ -0,0 +1,71 @@
import { SessionsRepository } from '$lib/server/api/repositories/sessions.repository';
import { sha256 } from '@oslojs/crypto/sha2';
import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding';
import { TimeSpan } from 'lucia';
import { inject, injectable } from 'tsyringe';
import type { Sessions, Users } from '../databases/postgres/tables';
export type SessionValidationResult = { session: Sessions; user: Users } | { session: null; user: null };
@injectable()
export class SessionsService {
constructor(@inject(SessionsRepository) private readonly sessionsRepository: SessionsRepository) {}
generateSessionToken() {
const bytes = new Uint8Array(20);
crypto.getRandomValues(bytes);
return encodeBase32LowerCaseNoPadding(bytes);
}
async createSession(
token: string,
userId: string,
ipCountry: string,
ipAddress: string,
twoFactorAuthEnabled: boolean,
isTwoFactorAuthenticated: boolean,
) {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const session: Sessions = {
id: sessionId,
userId,
expiresAt: new Date(Date.now() + new TimeSpan(2, 'w').seconds()),
ipCountry,
ipAddress,
twoFactorAuthEnabled,
isTwoFactorAuthenticated,
};
await this.sessionsRepository.create(session);
return session;
}
async validateSessionToken(token: string): Promise<SessionValidationResult> {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const sessions = await this.sessionsRepository.findBySessionId(sessionId);
if (sessions.length < 1) {
return {
session: null,
user: null,
};
}
const { user, session } = sessions[0];
if (Date.now() >= session.expiresAt.getTime()) {
await this.sessionsRepository.deleteBySessionId(sessionId);
return {
session: null,
user: null,
};
}
if (Date.now() >= session.expiresAt.getTime() - new TimeSpan(1, 'w').seconds()) {
session.expiresAt = new Date(Date.now() + new TimeSpan(2, 'w').seconds());
await this.sessionsRepository.updateSessionExpiresAt(sessionId, session.expiresAt);
}
return { session, user };
}
async invalidateSession(sessionId: string) {
await this.sessionsRepository.deleteBySessionId(sessionId);
}
}

View file

@ -2,7 +2,7 @@ import { CredentialsRepository } from '$lib/server/api/repositories/credentials.
import { decodeHex, encodeHexLowerCase } from '@oslojs/encoding'; import { decodeHex, encodeHexLowerCase } from '@oslojs/encoding';
import { verifyTOTP } from '@oslojs/otp'; import { verifyTOTP } from '@oslojs/otp';
import { inject, injectable } from 'tsyringe'; import { inject, injectable } from 'tsyringe';
import type { CredentialsType } from '../databases/tables'; import type { CredentialsType } from '../databases/postgres/tables';
@injectable() @injectable()
export class TotpService { export class TotpService {

View file

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

View file

@ -1,32 +1,32 @@
import 'reflect-metadata' import 'reflect-metadata';
import { IamService } from '$lib/server/api/services/iam.service' import { IamService } from '$lib/server/api/services/iam.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 { faker } from '@faker-js/faker' import { faker } from '@faker-js/faker';
import { container } from 'tsyringe' import { container } from 'tsyringe';
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
describe('IamService', () => { describe('IamService', () => {
let service: IamService let service: IamService;
const luciaService = vi.mocked(LuciaService.prototype) const luciaService = vi.mocked(SessionsService.prototype);
const userService = vi.mocked(UsersService.prototype) const userService = vi.mocked(UsersService.prototype);
beforeAll(() => { beforeAll(() => {
service = container service = container
.register<LuciaService>(LuciaService, { useValue: luciaService }) .register<SessionsService>(SessionsService, { useValue: luciaService })
.register<UsersService>(UsersService, { useValue: userService }) .register<UsersService>(UsersService, { useValue: userService })
.resolve(IamService) .resolve(IamService);
}) });
beforeEach(() => { beforeEach(() => {
vi.resetAllMocks() vi.resetAllMocks();
}) });
afterAll(() => { afterAll(() => {
vi.resetAllMocks() vi.resetAllMocks();
}) });
const timeStampDate = new Date() const timeStampDate = new Date();
const dbUser = { const dbUser = {
id: faker.string.uuid(), id: faker.string.uuid(),
cuid: 'ciglo1j8q0000t9j4xq8d6p5e', cuid: 'ciglo1j8q0000t9j4xq8d6p5e',
@ -40,85 +40,85 @@ describe('IamService', () => {
theme: 'system', theme: 'system',
createdAt: timeStampDate, createdAt: timeStampDate,
updatedAt: timeStampDate, updatedAt: timeStampDate,
} };
describe('Update Profile', () => { describe('Update Profile', () => {
it('should update user', async () => { it('should update user', async () => {
userService.findOneById = vi.fn().mockResolvedValueOnce(dbUser) userService.findOneById = vi.fn().mockResolvedValueOnce(dbUser);
userService.findOneByUsername = vi.fn().mockResolvedValue(undefined) userService.findOneByUsername = vi.fn().mockResolvedValue(undefined);
userService.updateUser = vi.fn().mockResolvedValue(dbUser) userService.updateUser = vi.fn().mockResolvedValue(dbUser);
const spy_userService_findOneById = vi.spyOn(userService, 'findOneById') const spy_userService_findOneById = vi.spyOn(userService, 'findOneById');
const spy_userService_findOneByUsername = vi.spyOn(userService, 'findOneByUsername') const spy_userService_findOneByUsername = vi.spyOn(userService, 'findOneByUsername');
const spy_userService_updateUser = vi.spyOn(userService, 'updateUser') const spy_userService_updateUser = vi.spyOn(userService, 'updateUser');
await expect( await expect(
service.updateProfile(faker.string.uuid(), { service.updateProfile(faker.string.uuid(), {
username: faker.internet.userName(), username: faker.internet.userName(),
}), }),
).resolves.toEqual(dbUser) ).resolves.toEqual(dbUser);
expect(spy_userService_findOneById).toBeCalledTimes(1) expect(spy_userService_findOneById).toBeCalledTimes(1);
expect(spy_userService_findOneByUsername).toBeCalledTimes(1) expect(spy_userService_findOneByUsername).toBeCalledTimes(1);
expect(spy_userService_updateUser).toBeCalledTimes(1) expect(spy_userService_updateUser).toBeCalledTimes(1);
}) });
it('should error on no user found', async () => { it('should error on no user found', async () => {
userService.findOneById = vi.fn().mockResolvedValueOnce(undefined) userService.findOneById = vi.fn().mockResolvedValueOnce(undefined);
const spy_userService_findOneById = vi.spyOn(userService, 'findOneById') const spy_userService_findOneById = vi.spyOn(userService, 'findOneById');
const spy_userService_findOneByUsername = vi.spyOn(userService, 'findOneByUsername') const spy_userService_findOneByUsername = vi.spyOn(userService, 'findOneByUsername');
const spy_userService_updateUser = vi.spyOn(userService, 'updateUser') const spy_userService_updateUser = vi.spyOn(userService, 'updateUser');
await expect( await expect(
service.updateProfile(faker.string.uuid(), { service.updateProfile(faker.string.uuid(), {
username: faker.internet.userName(), username: faker.internet.userName(),
}), }),
).resolves.toEqual({ ).resolves.toEqual({
error: 'User not found', error: 'User not found',
}) });
expect(spy_userService_findOneById).toBeCalledTimes(1) expect(spy_userService_findOneById).toBeCalledTimes(1);
expect(spy_userService_findOneByUsername).toBeCalledTimes(0) expect(spy_userService_findOneByUsername).toBeCalledTimes(0);
expect(spy_userService_updateUser).toBeCalledTimes(0) expect(spy_userService_updateUser).toBeCalledTimes(0);
}) });
it('should error on duplicate username', async () => { it('should error on duplicate username', async () => {
userService.findOneById = vi.fn().mockResolvedValueOnce(dbUser) userService.findOneById = vi.fn().mockResolvedValueOnce(dbUser);
userService.findOneByUsername = vi.fn().mockResolvedValue({ userService.findOneByUsername = vi.fn().mockResolvedValue({
id: faker.string.uuid(), id: faker.string.uuid(),
}) });
userService.updateUser = vi.fn().mockResolvedValue(dbUser) userService.updateUser = vi.fn().mockResolvedValue(dbUser);
const spy_userService_findOneById = vi.spyOn(userService, 'findOneById') const spy_userService_findOneById = vi.spyOn(userService, 'findOneById');
const spy_userService_findOneByUsername = vi.spyOn(userService, 'findOneByUsername') const spy_userService_findOneByUsername = vi.spyOn(userService, 'findOneByUsername');
const spy_userService_updateUser = vi.spyOn(userService, 'updateUser') const spy_userService_updateUser = vi.spyOn(userService, 'updateUser');
await expect( await expect(
service.updateProfile(faker.string.uuid(), { service.updateProfile(faker.string.uuid(), {
username: faker.internet.userName(), username: faker.internet.userName(),
}), }),
).resolves.toEqual({ ).resolves.toEqual({
error: 'Username already in use', error: 'Username already in use',
}) });
expect(spy_userService_findOneById).toBeCalledTimes(1) expect(spy_userService_findOneById).toBeCalledTimes(1);
expect(spy_userService_findOneByUsername).toBeCalledTimes(1) expect(spy_userService_findOneByUsername).toBeCalledTimes(1);
expect(spy_userService_updateUser).toBeCalledTimes(0) expect(spy_userService_updateUser).toBeCalledTimes(0);
}) });
}) });
it('should not error if the user id of new username is the current user id', async () => { it('should not error if the user id of new username is the current user id', async () => {
userService.findOneById = vi.fn().mockResolvedValueOnce(dbUser) userService.findOneById = vi.fn().mockResolvedValueOnce(dbUser);
userService.findOneByUsername = vi.fn().mockResolvedValue({ userService.findOneByUsername = vi.fn().mockResolvedValue({
id: dbUser.id, id: dbUser.id,
}) });
userService.updateUser = vi.fn().mockResolvedValue(dbUser) userService.updateUser = vi.fn().mockResolvedValue(dbUser);
const spy_userService_findOneById = vi.spyOn(userService, 'findOneById') const spy_userService_findOneById = vi.spyOn(userService, 'findOneById');
const spy_userService_findOneByUsername = vi.spyOn(userService, 'findOneByUsername') const spy_userService_findOneByUsername = vi.spyOn(userService, 'findOneByUsername');
const spy_userService_updateUser = vi.spyOn(userService, 'updateUser') const spy_userService_updateUser = vi.spyOn(userService, 'updateUser');
await expect( await expect(
service.updateProfile(dbUser.id, { service.updateProfile(dbUser.id, {
username: dbUser.id, username: dbUser.id,
}), }),
).resolves.toEqual(dbUser) ).resolves.toEqual(dbUser);
expect(spy_userService_findOneById).toBeCalledTimes(1) expect(spy_userService_findOneById).toBeCalledTimes(1);
expect(spy_userService_findOneByUsername).toBeCalledTimes(1) expect(spy_userService_findOneByUsername).toBeCalledTimes(1);
expect(spy_userService_updateUser).toBeCalledTimes(1) expect(spy_userService_updateUser).toBeCalledTimes(1);
}) });
}) });

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