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

View file

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

View file

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

View file

@ -5,8 +5,8 @@
"scripts": {
"db:push": "drizzle-kit push",
"db:generate": "drizzle-kit generate",
"db:migrate": "tsx src/lib/server/api/databases/migrate.ts",
"db:seed": "tsx src/lib/server/api/databases/seed.ts",
"db:migrate": "tsx src/lib/server/api/databases/postgres/migrate.ts",
"db:seed": "tsx src/lib/server/api/databases/postgres/seed.ts",
"db:studio": "drizzle-kit studio --verbose",
"dev": "NODE_OPTIONS=\"--inspect\" vite dev --host",
"build": "vite build",
@ -27,20 +27,20 @@
"@faker-js/faker": "^8.4.1",
"@melt-ui/pp": "^0.3.2",
"@melt-ui/svelte": "^0.83.0",
"@playwright/test": "^1.48.0",
"@sveltejs/adapter-auto": "^3.2.5",
"@sveltejs/enhanced-img": "^0.3.9",
"@sveltejs/kit": "^2.7.1",
"@playwright/test": "^1.48.2",
"@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/enhanced-img": "^0.3.10",
"@sveltejs/kit": "^2.7.5",
"@sveltejs/vite-plugin-svelte": "4.0.0-next.7",
"@types/cookie": "^0.6.0",
"@types/node": "^20.16.11",
"@types/node": "^20.17.6",
"@types/pg": "^8.11.10",
"@types/qrcode": "^1.5.5",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"arctic": "^1.9.2",
"autoprefixer": "^10.4.20",
"drizzle-kit": "^0.23.2",
"drizzle-kit": "^0.27.1",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "2.36.0-next.13",
@ -48,7 +48,7 @@
"just-debounce-it": "^3.2.0",
"lucia": "3.2.0",
"lucide-svelte": "^0.408.0",
"nodemailer": "^6.9.15",
"nodemailer": "^6.9.16",
"postcss": "^8.4.47",
"postcss-import": "^16.1.0",
"postcss-load-config": "^5.1.0",
@ -57,18 +57,18 @@
"prettier-plugin-svelte": "^3.2.7",
"svelte": "5.0.0-next.175",
"svelte-check": "^3.8.6",
"svelte-headless-table": "^0.18.2",
"svelte-headless-table": "^0.18.3",
"svelte-meta-tags": "^3.1.4",
"svelte-preprocess": "^6.0.3",
"svelte-sequential-preprocessor": "^2.0.2",
"sveltekit-flash-message": "^2.4.4",
"sveltekit-superforms": "^2.19.1",
"tailwindcss": "^3.4.13",
"sveltekit-superforms": "^2.20.0",
"tailwindcss": "^3.4.14",
"ts-node": "^10.9.2",
"tslib": "^2.7.0",
"tsx": "^4.19.1",
"tslib": "^2.8.1",
"tsx": "^4.19.2",
"typescript": "^5.6.3",
"vite": "^5.4.9",
"vite": "^5.4.10",
"vitest": "^1.6.0",
"zod": "^3.23.8"
},
@ -92,27 +92,27 @@
"@oslojs/otp": "^1.0.0",
"@oslojs/webauthn": "^1.0.0",
"@paralleldrive/cuid2": "^2.2.2",
"@scalar/hono-api-reference": "^0.5.154",
"@sveltejs/adapter-node": "^5.2.7",
"@sveltejs/adapter-vercel": "^5.4.5",
"@scalar/hono-api-reference": "^0.5.158",
"@sveltejs/adapter-node": "^5.2.9",
"@sveltejs/adapter-vercel": "^5.4.6",
"@types/feather-icons": "^4.29.4",
"bits-ui": "^0.21.16",
"boardgamegeekclient": "^1.9.1",
"bullmq": "^5.20.0",
"bullmq": "^5.24.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cookie": "^0.6.0",
"cookie": "^1.0.1",
"dotenv": "^16.4.5",
"dotenv-expand": "^11.0.6",
"drizzle-orm": "^0.32.2",
"drizzle-orm": "^0.36.0",
"drizzle-zod": "^0.5.1",
"feather-icons": "^4.29.2",
"formsnap": "^1.0.1",
"handlebars": "^4.7.8",
"hono": "^4.6.4",
"hono": "^4.6.9",
"hono-pino": "^0.3.0",
"hono-rate-limiter": "^0.4.0",
"hono-zod-openapi": "^0.3.0",
"hono-zod-openapi": "^0.3.1",
"html-entities": "^2.5.2",
"iconify-icon": "^2.1.0",
"ioredis": "^5.4.1",
@ -122,21 +122,21 @@
"mode-watcher": "^0.4.1",
"open-props": "^1.7.7",
"oslo": "^1.2.1",
"pg": "^8.13.0",
"pino": "^9.4.0",
"pino-pretty": "^11.2.2",
"postgres": "^3.4.4",
"pg": "^8.13.1",
"pino": "^9.5.0",
"pino-pretty": "^11.3.0",
"postgres": "^3.4.5",
"qrcode": "^1.5.4",
"radix-svelte": "^0.9.0",
"rate-limit-redis": "^4.2.0",
"reflect-metadata": "^0.2.2",
"stoker": "^1.2.3",
"stoker": "^1.3.0",
"svelte-lazy-loader": "^1.0.0",
"svelte-sonner": "^0.3.28",
"tailwind-merge": "^2.5.4",
"tailwind-variants": "^0.2.1",
"tailwindcss-animate": "^1.0.7",
"tsyringe": "^4.8.0",
"zod-to-json-schema": "^3.23.3"
"zod-to-json-schema": "^3.23.5"
}
}

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

@ -1,11 +1,11 @@
import { StatusCodes } from '$lib/constants/status-codes';
import { unauthorizedSchema } from '$lib/server/api/common/exceptions';
import cuidParamsSchema from '$lib/server/api/common/openapi/cuidParamsSchema';
import { selectCollectionSchema } from '$lib/server/api/databases/tables';
import { z } from '@hono/zod-openapi';
import { IdParamsSchema } from 'stoker/openapi/schemas';
import { createErrorSchema } from 'stoker/openapi/schemas';
import { taggedAuthRoute } from '../common/openapi/create-auth-route';
import { selectCollectionSchema } from '../databases/postgres/tables';
const tag = 'Collection';

View file

@ -1,5 +1,6 @@
import { StatusCodes } from '$lib/constants/status-codes';
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 { updateEmailDto } from '$lib/server/api/dtos/update-email.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 { IamService } from '$lib/server/api/services/iam.service';
import { LoginRequestsService } from '$lib/server/api/services/loginrequest.service';
import { LuciaService } from '$lib/server/api/services/lucia.service';
import { SessionsService } from '$lib/server/api/services/sessions.service';
import { zValidator } from '@hono/zod-validator';
import { openApi } from 'hono-zod-openapi';
import { setCookie } from 'hono/cookie';
@ -20,7 +21,7 @@ export class IamController extends Controller {
constructor(
@inject(IamService) private readonly iamService: IamService,
@inject(LoginRequestsService) private readonly loginRequestService: LoginRequestsService,
@inject(LuciaService) private luciaService: LuciaService,
@inject(SessionsService) private sessionsService: SessionsService,
) {
super();
}
@ -78,18 +79,9 @@ export class IamController extends Controller {
}
try {
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);
const sessionCookie = this.luciaService.lucia.createBlankSessionCookie();
setCookie(c, sessionCookie.name, sessionCookie.value, {
path: sessionCookie.attributes.path,
maxAge: sessionCookie.attributes.maxAge,
domain: sessionCookie.attributes.domain,
sameSite: sessionCookie.attributes.sameSite as any,
secure: sessionCookie.attributes.secure,
httpOnly: sessionCookie.attributes.httpOnly,
expires: sessionCookie.attributes.expires,
});
deleteSessionTokenCookie(c);
return c.json({ status: 'success' });
} catch (error) {
console.error('Error updating password', error);
@ -116,16 +108,7 @@ export class IamController extends Controller {
.post('/logout', requireAuth, openApi(logout), async (c) => {
const sessionId = c.var.session.id;
await this.iamService.logout(sessionId);
const sessionCookie = this.luciaService.lucia.createBlankSessionCookie();
setCookie(c, sessionCookie.name, sessionCookie.value, {
path: sessionCookie.attributes.path,
maxAge: sessionCookie.attributes.maxAge,
domain: sessionCookie.attributes.domain,
sameSite: sessionCookie.attributes.sameSite as any,
secure: sessionCookie.attributes.secure,
httpOnly: sessionCookie.attributes.httpOnly,
expires: sessionCookie.attributes.expires,
});
deleteSessionTokenCookie(c);
return c.json({ status: 'success' });
});
}

View file

@ -1,6 +1,6 @@
import { StatusCodes } from '$lib/constants/status-codes';
import { unauthorizedSchema } from '$lib/server/api/common/exceptions';
import { selectUserSchema } from '$lib/server/api/databases/tables/users.table';
import { selectUserSchema } from '$lib/server/api/databases/postgres/tables/users.table';
import { updateProfileDto } from '$lib/server/api/dtos/update-profile.dto';
import { createErrorSchema } from 'stoker/openapi/schemas';
import { taggedAuthRoute } from '../common/openapi/create-auth-route';

View file

@ -1,44 +1,50 @@
import 'reflect-metadata'
import { Controller } from '$lib/server/api/common/types/controller'
import { signinUsernameDto } from '$lib/server/api/dtos/signin-username.dto'
import { LuciaService } from '$lib/server/api/services/lucia.service'
import { zValidator } from '@hono/zod-validator'
import { setCookie } from 'hono/cookie'
import { TimeSpan } from 'oslo'
import 'reflect-metadata';
import { Controller } from '$lib/server/api/common/types/controller';
import { signinUsernameDto } from '$lib/server/api/dtos/signin-username.dto';
import { SessionsService } from '$lib/server/api/services/sessions.service';
import { zValidator } from '@hono/zod-validator';
import { openApi } from 'hono-zod-openapi';
import { inject, injectable } from 'tsyringe'
import { limiter } from '../middleware/rate-limiter.middleware'
import { LoginRequestsService } from '../services/loginrequest.service'
import { signinUsername } from './login.routes'
import { setCookie } from 'hono/cookie';
import { TimeSpan } from 'oslo';
import { inject, injectable } from 'tsyringe';
import { limiter } from '../middleware/rate-limiter.middleware';
import { LoginRequestsService } from '../services/loginrequest.service';
import { signinUsername } from './login.routes';
@injectable()
export class LoginController extends Controller {
constructor(
@inject(LoginRequestsService) private readonly loginRequestsService: LoginRequestsService,
@inject(LuciaService) private luciaService: LuciaService,
@inject(SessionsService) private luciaService: SessionsService,
) {
super()
super();
}
routes() {
return this.controller.post('/', openApi(signinUsername), zValidator('json', signinUsernameDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
const { username, password } = c.req.valid('json')
const session = await this.loginRequestsService.verify({ username, password }, c.req)
const sessionCookie = this.luciaService.lucia.createSessionCookie(session.id)
console.log('set cookie', sessionCookie)
setCookie(c, sessionCookie.name, sessionCookie.value, {
path: sessionCookie.attributes.path,
maxAge:
sessionCookie?.attributes?.maxAge && sessionCookie?.attributes?.maxAge < new TimeSpan(365, 'd').seconds()
? sessionCookie.attributes.maxAge
: new TimeSpan(2, 'w').seconds(),
domain: sessionCookie.attributes.domain,
sameSite: sessionCookie.attributes.sameSite as any,
secure: sessionCookie.attributes.secure,
httpOnly: sessionCookie.attributes.httpOnly,
expires: sessionCookie.attributes.expires,
})
return c.json({ message: 'ok' })
})
return this.controller.post(
'/',
openApi(signinUsername),
zValidator('json', signinUsernameDto),
limiter({ limit: 10, minutes: 60 }),
async (c) => {
const { username, password } = c.req.valid('json');
const session = await this.loginRequestsService.verify({ username, password }, c.req);
const sessionCookie = this.luciaService.lucia.createSessionCookie(session.id);
console.log('set cookie', sessionCookie);
setCookie(c, sessionCookie.name, sessionCookie.value, {
path: sessionCookie.attributes.path,
maxAge:
sessionCookie?.attributes?.maxAge && sessionCookie?.attributes?.maxAge < new TimeSpan(365, 'd').seconds()
? sessionCookie.attributes.maxAge
: new TimeSpan(2, 'w').seconds(),
domain: sessionCookie.attributes.domain,
sameSite: sessionCookie.attributes.sameSite as any,
secure: sessionCookie.attributes.secure,
httpOnly: sessionCookie.attributes.httpOnly,
expires: sessionCookie.attributes.expires,
});
return c.json({ message: 'ok' });
},
);
}
}

View file

@ -1,14 +1,14 @@
import 'reflect-metadata'
import { StatusCodes } from '$lib/constants/status-codes'
import { Controller } from '$lib/server/api/common/types/controller'
import { verifyTotpDto } from '$lib/server/api/dtos/verify-totp.dto'
import { RecoveryCodesService } from '$lib/server/api/services/recovery-codes.service'
import { TotpService } from '$lib/server/api/services/totp.service'
import { UsersService } from '$lib/server/api/services/users.service'
import { zValidator } from '@hono/zod-validator'
import { inject, injectable } from 'tsyringe'
import { CredentialsType } from '../databases/tables'
import { requireAuth } from '../middleware/require-auth.middleware'
import 'reflect-metadata';
import { StatusCodes } from '$lib/constants/status-codes';
import { Controller } from '$lib/server/api/common/types/controller';
import { verifyTotpDto } from '$lib/server/api/dtos/verify-totp.dto';
import { RecoveryCodesService } from '$lib/server/api/services/recovery-codes.service';
import { TotpService } from '$lib/server/api/services/totp.service';
import { UsersService } from '$lib/server/api/services/users.service';
import { zValidator } from '@hono/zod-validator';
import { inject, injectable } from 'tsyringe';
import { CredentialsType } from '../databases/postgres/tables';
import { requireAuth } from '../middleware/require-auth.middleware';
@injectable()
export class MfaController extends Controller {
@ -17,59 +17,59 @@ export class MfaController extends Controller {
@inject(TotpService) private readonly totpService: TotpService,
@inject(UsersService) private readonly usersService: UsersService,
) {
super()
super();
}
routes() {
return this.controller
.get('/totp', requireAuth, async (c) => {
const user = c.var.user
const totpCredential = await this.totpService.findOneByUserId(user.id)
return c.json({ totpCredential })
const user = c.var.user;
const totpCredential = await this.totpService.findOneByUserId(user.id);
return c.json({ totpCredential });
})
.post('/totp', requireAuth, async (c) => {
const user = c.var.user
const totpCredential = await this.totpService.create(user.id)
return c.json({ totpCredential })
const user = c.var.user;
const totpCredential = await this.totpService.create(user.id);
return c.json({ totpCredential });
})
.delete('/totp', requireAuth, async (c) => {
const user = c.var.user
const user = c.var.user;
try {
await this.totpService.deleteOneByUserIdAndType(user.id, CredentialsType.TOTP)
await this.recoveryCodesService.deleteAllRecoveryCodesByUserId(user.id)
await this.usersService.updateUser(user.id, { mfa_enabled: false })
console.log('TOTP deleted')
return c.body(null, StatusCodes.NO_CONTENT)
await this.totpService.deleteOneByUserIdAndType(user.id, CredentialsType.TOTP);
await this.recoveryCodesService.deleteAllRecoveryCodesByUserId(user.id);
await this.usersService.updateUser(user.id, { mfa_enabled: false });
console.log('TOTP deleted');
return c.body(null, StatusCodes.NO_CONTENT);
} catch (e) {
console.error(e)
return c.status(StatusCodes.INTERNAL_SERVER_ERROR)
console.error(e);
return c.status(StatusCodes.INTERNAL_SERVER_ERROR);
}
})
.get('/totp/recoveryCodes', requireAuth, async (c) => {
const user = c.var.user
const user = c.var.user;
// You can only view recovery codes once and that is on creation
const existingCodes = await this.recoveryCodesService.findAllRecoveryCodesByUserId(user.id)
const existingCodes = await this.recoveryCodesService.findAllRecoveryCodesByUserId(user.id);
if (existingCodes && existingCodes.length > 0) {
console.log('Recovery Codes found', existingCodes)
return c.json({ recoveryCodes: existingCodes })
console.log('Recovery Codes found', existingCodes);
return c.json({ recoveryCodes: existingCodes });
}
const recoveryCodes = await this.recoveryCodesService.createRecoveryCodes(user.id)
return c.json({ recoveryCodes })
const recoveryCodes = await this.recoveryCodesService.createRecoveryCodes(user.id);
return c.json({ recoveryCodes });
})
.post('/totp/verify', requireAuth, zValidator('json', verifyTotpDto), async (c) => {
try {
const user = c.var.user
const { code } = c.req.valid('json')
const verified = await this.totpService.verify(user.id, code)
const user = c.var.user;
const { code } = c.req.valid('json');
const verified = await this.totpService.verify(user.id, code);
if (verified) {
await this.usersService.updateUser(user.id, { mfa_enabled: true })
return c.json({}, StatusCodes.OK)
await this.usersService.updateUser(user.id, { mfa_enabled: true });
return c.json({}, StatusCodes.OK);
}
return c.json('Invalid code', StatusCodes.BAD_REQUEST)
return c.json('Invalid code', StatusCodes.BAD_REQUEST);
} catch (e) {
console.error(e)
return c.status(StatusCodes.INTERNAL_SERVER_ERROR)
console.error(e);
return c.status(StatusCodes.INTERNAL_SERVER_ERROR);
}
})
});
}
}

View file

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

View file

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

View file

@ -1,30 +0,0 @@
import 'dotenv/config'
import { drizzle } from 'drizzle-orm/postgres-js'
import { migrate } from 'drizzle-orm/postgres-js/migrator'
import postgres from 'postgres'
import config from '../../../../../drizzle.config'
import env from '../common/env'
const connection = postgres({
host: env.DATABASE_HOST || 'localhost',
port: env.DATABASE_PORT,
user: env.DATABASE_USER || 'root',
password: env.DATABASE_PASSWORD || '',
database: env.DATABASE_DB || 'boredgame',
ssl: false, // env.NODE_ENV === 'development' ? false : 'require',
max: 1,
})
const db = drizzle(connection)
try {
if (!config.out) {
console.error('No migrations folder specified in drizzle.config.ts')
process.exit()
}
await migrate(db, { migrationsFolder: config.out })
console.log('Migrations complete')
} catch (e) {
console.error(e)
}
process.exit()

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,11 @@
import type { 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 type { db } from '../../packages/drizzle';
import { HashingService } from '../../services/hashing.service';
import type { db } from '../../../packages/drizzle';
import { HashingService } from '../../../services/hashing.service';
import * as schema from '../tables';
import users from './data/users.json';

View file

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

View file

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

View file

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

View file

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

View file

@ -2,19 +2,19 @@ import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { createSelectSchema } from 'drizzle-zod';
import { timestamps } from '../../common/utils/table';
import { timestamps } from '../../../common/utils/table';
import { collection_items } from './collectionItems.table';
import { usersTable } from './users.table';
export const collections = pgTable('collections', {
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
id: uuid().primaryKey().defaultRandom(),
cuid: text()
.unique()
.$defaultFn(() => cuid2()),
user_id: uuid('user_id')
user_id: uuid()
.notNull()
.references(() => usersTable.id, { onDelete: 'cascade' }),
name: text('name').notNull().default('My Collection'),
name: text().notNull().default('My Collection'),
...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 { type InferSelectModel, relations } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../../common/utils/table'
import { gamesTable } from '././games.table'
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { timestamps } from '../../../common/utils/table';
import { gamesTable } from './games.table';
export const expansionsTable = pgTable('expansions', {
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
id: uuid().primaryKey().defaultRandom(),
cuid: text()
.unique()
.$defaultFn(() => cuid2()),
base_game_id: uuid('base_game_id')
base_game_id: uuid()
.notNull()
.references(() => gamesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
game_id: uuid('game_id')
game_id: uuid()
.notNull()
.references(() => gamesTable.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
...timestamps,
})
});
export type Expansions = InferSelectModel<typeof expansionsTable>
export type Expansions = InferSelectModel<typeof expansionsTable>;
export const expansion_relations = relations(expansionsTable, ({ one }) => ({
baseGame: one(gamesTable, {
@ -29,4 +29,4 @@ export const expansion_relations = relations(expansionsTable, ({ one }) => ({
fields: [expansionsTable.game_id],
references: [gamesTable.id],
}),
}))
}));

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,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 { createMiddleware } from 'hono/factory';
import { verifyRequestOrigin } from 'oslo/request';
@ -6,7 +7,7 @@ import { container } from 'tsyringe';
import type { AppBindings } from '../common/types/hono';
// resolve dependencies from the container
const { lucia } = container.resolve(LuciaService);
const sessionService = container.resolve(SessionsService);
export const verifyOrigin: MiddlewareHandler<AppBindings> = createMiddleware(async (c, next) => {
if (c.req.method === 'GET') {
@ -30,7 +31,7 @@ export const validateAuthSession: MiddlewareHandler<AppBindings> = createMiddlew
const { session, user } = await lucia.validateSession(sessionId);
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) {
c.header('Set-Cookie', lucia.createBlankSessionCookie().serialize(), { append: true });

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,8 @@
import { DrizzleService } from '$lib/server/api/services/drizzle.service'
import { type InferInsertModel, eq } from 'drizzle-orm'
import { inject, injectable } from 'tsyringe'
import { takeFirstOrThrow } from '../common/utils/repository'
import { user_roles } from '../databases/tables'
import { DrizzleService } from '$lib/server/api/services/drizzle.service';
import { type InferInsertModel, eq } from 'drizzle-orm';
import { inject, injectable } from 'tsyringe';
import { takeFirstOrThrow } from '../common/utils/repository';
import { user_roles } from '../databases/postgres/tables';
/* -------------------------------------------------------------------------- */
/* Repository */
@ -20,8 +20,8 @@ storing data. They should not contain any business logic, only database queries.
In our case the method 'trxHost' is used to set the transaction context.
*/
export type CreateUserRole = InferInsertModel<typeof user_roles>
export type UpdateUserRole = Partial<CreateUserRole>
export type CreateUserRole = InferInsertModel<typeof user_roles>;
export type UpdateUserRole = Partial<CreateUserRole>;
@injectable()
export class UserRolesRepository {
@ -30,26 +30,26 @@ export class UserRolesRepository {
async findOneById(id: string, db = this.drizzle.db) {
return db.query.user_roles.findFirst({
where: eq(user_roles.id, id),
})
});
}
async findOneByIdOrThrow(id: string, db = this.drizzle.db) {
const userRole = await this.findOneById(id, db)
if (!userRole) throw Error('User not found')
return userRole
const userRole = await this.findOneById(id, db);
if (!userRole) throw Error('User not found');
return userRole;
}
async findAllByUserId(userId: string, db = this.drizzle.db) {
return db.query.user_roles.findMany({
where: eq(user_roles.user_id, userId),
})
});
}
async create(data: CreateUserRole, db = this.drizzle.db) {
return db.insert(user_roles).values(data).returning().then(takeFirstOrThrow)
return db.insert(user_roles).values(data).returning().then(takeFirstOrThrow);
}
async delete(id: string, db = this.drizzle.db) {
return db.delete(user_roles).where(eq(user_roles.id, id)).returning().then(takeFirstOrThrow)
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 { type InferInsertModel, eq } from 'drizzle-orm';
import { inject, injectable } from 'tsyringe';

View file

@ -1,18 +1,18 @@
import { DrizzleService } from '$lib/server/api/services/drizzle.service'
import { type InferInsertModel, eq } from 'drizzle-orm'
import { inject, injectable } from 'tsyringe'
import { takeFirstOrThrow } from '../common/utils/repository'
import { wishlistsTable } from '../databases/tables'
import { DrizzleService } from '$lib/server/api/services/drizzle.service';
import { type InferInsertModel, eq } from 'drizzle-orm';
import { inject, injectable } from 'tsyringe';
import { takeFirstOrThrow } from '../common/utils/repository';
import { wishlistsTable } from '../databases/postgres/tables';
export type CreateWishlist = InferInsertModel<typeof wishlistsTable>
export type UpdateWishlist = Partial<CreateWishlist>
export type CreateWishlist = InferInsertModel<typeof wishlistsTable>;
export type UpdateWishlist = Partial<CreateWishlist>;
@injectable()
export class WishlistsRepository {
constructor(@inject(DrizzleService) private readonly drizzle: DrizzleService) {}
async findAll(db = this.drizzle.db) {
return db.query.wishlistsTable.findMany()
return db.query.wishlistsTable.findMany();
}
async findOneById(id: string, db = this.drizzle.db) {
@ -22,7 +22,7 @@ export class WishlistsRepository {
cuid: true,
name: true,
},
})
});
}
async findOneByCuid(cuid: string, db = this.drizzle.db) {
@ -32,7 +32,7 @@ export class WishlistsRepository {
cuid: true,
name: true,
},
})
});
}
async findOneByUserId(userId: string, db = this.drizzle.db) {
@ -42,7 +42,7 @@ export class WishlistsRepository {
cuid: true,
name: true,
},
})
});
}
async findAllByUserId(userId: string, db = this.drizzle.db) {
@ -53,14 +53,14 @@ export class WishlistsRepository {
name: true,
createdAt: true,
},
})
});
}
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) {
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 { type Disposable, injectable } from 'tsyringe';
import { config } from '../common/config';
import * as schema from '../databases/tables';
import * as schema from '../databases/postgres/tables';
@injectable()
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 { UpdateEmailDto } from '$lib/server/api/dtos/update-email.dto'
import type { UpdateProfileDto } from '$lib/server/api/dtos/update-profile.dto'
import type { VerifyPasswordDto } from '$lib/server/api/dtos/verify-password.dto'
import { LuciaService } from '$lib/server/api/services/lucia.service'
import { UsersService } from '$lib/server/api/services/users.service'
import { inject, injectable } from 'tsyringe'
import type { ChangePasswordDto } from '$lib/server/api/dtos/change-password.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 { VerifyPasswordDto } from '$lib/server/api/dtos/verify-password.dto';
import { SessionsService } from '$lib/server/api/services/sessions.service';
import { UsersService } from '$lib/server/api/services/users.service';
import { inject, injectable } from 'tsyringe';
import { CredentialsType } from '../databases/postgres/tables';
/* -------------------------------------------------------------------------- */
/* Service */
@ -27,60 +27,60 @@ simple as possible. This makes the service easier to read, test and understand.
@injectable()
export class IamService {
constructor(
@inject(LuciaService) private luciaService: LuciaService,
@inject(SessionsService) private luciaService: SessionsService,
@inject(UsersService) private readonly usersService: UsersService,
) {}
async logout(sessionId: string) {
return this.luciaService.lucia.invalidateSession(sessionId)
return this.luciaService.lucia.invalidateSession(sessionId);
}
async updateProfile(userId: string, data: UpdateProfileDto) {
const user = await this.usersService.findOneById(userId)
const user = await this.usersService.findOneById(userId);
if (!user) {
return {
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) {
return {
error: 'Username already in use',
}
};
}
return this.usersService.updateUser(user.id, {
first_name: data.firstName,
last_name: data.lastName,
username: data.username !== user.username ? data.username : user.username,
})
});
}
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) {
return null
return null;
}
return this.usersService.updateUser(userId, {
email,
})
});
}
async updatePassword(userId: string, data: ChangePasswordDto) {
const { password } = data
await this.usersService.updatePassword(userId, password)
const { password } = data;
await this.usersService.updatePassword(userId, password);
}
async verifyPassword(userId: string, data: VerifyPasswordDto) {
const user = await this.usersService.findOneById(userId)
const user = await this.usersService.findOneById(userId);
if (!user) {
return null
return null;
}
const { password } = data
return this.usersService.verifyPassword(userId, { password })
const { password } = data;
return this.usersService.verifyPassword(userId, { password });
}
}

View file

@ -1,19 +1,19 @@
import type { SigninUsernameDto } from '$lib/server/api/dtos/signin-username.dto'
import { LuciaService } from '$lib/server/api/services/lucia.service'
import type { HonoRequest } from 'hono'
import { inject, injectable } from 'tsyringe'
import { BadRequest } from '../common/exceptions'
import type { Credentials } from '../databases/tables'
import { DatabaseProvider } from '../providers/database.provider'
import { CredentialsRepository } from '../repositories/credentials.repository'
import { UsersRepository } from '../repositories/users.repository'
import { MailerService } from './mailer.service'
import { TokensService } from './tokens.service'
import type { SigninUsernameDto } from '$lib/server/api/dtos/signin-username.dto';
import { SessionsService } from '$lib/server/api/services/sessions.service';
import type { HonoRequest } from 'hono';
import { inject, injectable } from 'tsyringe';
import { BadRequest } from '../common/exceptions';
import type { Credentials } from '../databases/postgres/tables';
import { DatabaseProvider } from '../providers/database.provider';
import { CredentialsRepository } from '../repositories/credentials.repository';
import { UsersRepository } from '../repositories/users.repository';
import { MailerService } from './mailer.service';
import { TokensService } from './tokens.service';
@injectable()
export class LoginRequestsService {
constructor(
@inject(LuciaService) private luciaService: LuciaService,
@inject(SessionsService) private sessionsService: SessionsService,
@inject(DatabaseProvider) private readonly db: DatabaseProvider,
@inject(TokensService) private readonly tokensService: TokensService,
@inject(MailerService) private readonly mailerService: MailerService,
@ -34,46 +34,48 @@ export class LoginRequestsService {
// }
async verify(data: SigninUsernameDto, req: HonoRequest) {
const requestIpAddress = req.header('x-real-ip')
const requestIpCountry = req.header('x-vercel-ip-country')
const existingUser = await this.usersRepository.findOneByUsername(data.username)
const requestIpAddress = req.header('x-real-ip');
const requestIpCountry = req.header('x-vercel-ip-country');
const existingUser = await this.usersRepository.findOneByUsername(data.username);
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) {
throw BadRequest('Invalid credentials')
throw BadRequest('Invalid credentials');
}
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) {
const requestIpAddress = req.header('x-real-ip')
const requestIpCountry = req.header('x-vercel-ip-country')
return this.luciaService.lucia.createSession(existingUserId, {
ip_country: requestIpCountry || 'unknown',
ip_address: requestIpAddress || 'unknown',
twoFactorAuthEnabled: !!totpCredentials && totpCredentials?.secret_data !== null && totpCredentials?.secret_data !== '',
isTwoFactorAuthenticated: false,
})
const requestIpAddress = req.header('x-real-ip');
const requestIpCountry = req.header('x-vercel-ip-country');
return this.sessionsService.createSession(
this.sessionsService.generateSessionToken(),
existingUserId,
requestIpCountry || 'unknown',
requestIpAddress || 'unknown',
!!totpCredentials && totpCredentials?.secret_data !== null && totpCredentials?.secret_data !== '',
false,
);
}
// Create a new user and send a welcome email - or other onboarding process
private async handleNewUserRegistration(email: string) {
const newUser = await this.usersRepository.create({ email, verified: true })
this.mailerService.sendWelcome({ to: email, props: null })
const newUser = await this.usersRepository.create({ email, verified: true });
this.mailerService.sendWelcome({ to: email, props: null });
// 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

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 { verifyTOTP } from '@oslojs/otp';
import { inject, injectable } from 'tsyringe';
import type { CredentialsType } from '../databases/tables';
import type { CredentialsType } from '../databases/postgres/tables';
@injectable()
export class TotpService {

View file

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

View file

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

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