Refactoring to match simplifying done on the origin TaroStack.

This commit is contained in:
Bradley Shellnut 2024-09-01 12:22:00 -07:00
parent 16f00607b1
commit 3aa537f389
137 changed files with 2344 additions and 2405 deletions

2
.gitignore vendored
View file

@ -7,6 +7,8 @@ node_modules
.env.* .env.*
*.xdp* *.xdp*
!.env.example !.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
.vercel .vercel
.output .output
.idea .idea

View file

@ -1,11 +1,11 @@
import 'dotenv/config'; import 'dotenv/config'
import { defineConfig } from 'drizzle-kit'; import { defineConfig } from 'drizzle-kit'
import env from './src/env'; import env from './src/env'
export default defineConfig({ export default defineConfig({
dialect: 'postgresql', dialect: 'postgresql',
out: './src/lib/server/api/infrastructure/database/migrations', out: './src/lib/server/api/databases/migrations',
schema: './src/lib/server/api/infrastructure/database/tables/index.ts', schema: './src/lib/server/api/databases/tables/drizzle.ts',
dbCredentials: { dbCredentials: {
host: env.DATABASE_HOST || 'localhost', host: env.DATABASE_HOST || 'localhost',
port: Number(env.DATABASE_PORT) || 5432, port: Number(env.DATABASE_PORT) || 5432,
@ -20,6 +20,6 @@ export default defineConfig({
strict: true, strict: true,
migrations: { migrations: {
table: 'migrations', table: 'migrations',
schema: 'public' schema: 'public',
} },
}); })

View file

@ -1,32 +1,32 @@
import { error, json } from '@sveltejs/kit'; import { collection_items, usersTable } from '$db/schema'
import { eq } from 'drizzle-orm'; import { error, json } from '@sveltejs/kit'
import {db} from '$lib/server/api/infrastructure/database'; import { eq } from 'drizzle-orm'
import { collection_items, usersTable } from '$db/schema'; import { db } from '../../../../src/lib/server/api/packages/drizzle'
// Search a user's collection // Search a user's collection
export async function GET({ url, locals, params }) { export async function GET({ url, locals, params }) {
const searchParams = Object.fromEntries(url.searchParams); const searchParams = Object.fromEntries(url.searchParams)
const q = searchParams?.q || ''; const q = searchParams?.q || ''
const limit = Number.parseInt(searchParams?.limit) || 10; const limit = Number.parseInt(searchParams?.limit) || 10
const skip = Number.parseInt(searchParams?.skip) || 0; const skip = Number.parseInt(searchParams?.skip) || 0
const order = searchParams?.order || 'asc'; const order = searchParams?.order || 'asc'
const sort = searchParams?.sort || 'name'; const sort = searchParams?.sort || 'name'
const collection_id = params.id; const collection_id = params.id
console.log('url', url); console.log('url', url)
console.log('username', locals?.user?.id); console.log('username', locals?.user?.id)
if (!locals.user) { if (!locals.user) {
error(401, { message: 'Unauthorized' }); error(401, { message: 'Unauthorized' })
} }
const collection = await db.query.collections.findFirst({ const collection = await db.query.collections.findFirst({
where: eq(usersTable.id, locals?.user?.id), where: eq(usersTable.id, locals?.user?.id),
}); })
console.log('collection', collection); console.log('collection', collection)
if (!collection) { if (!collection) {
console.log('Collection was not found'); console.log('Collection was not found')
error(404, { message: 'Collection was not found' }); error(404, { message: 'Collection was not found' })
} }
try { try {
@ -42,21 +42,20 @@ export async function GET({ url, locals, params }) {
}, },
}, },
orderBy: (collection_items, { asc, desc }) => { orderBy: (collection_items, { asc, desc }) => {
const dbSort = const dbSort = sort === 'dateAdded' ? collection_items.created_at : collection_items.times_played
sort === 'dateAdded' ? collection_items.created_at : collection_items.times_played;
if (order === 'asc') { if (order === 'asc') {
return asc(dbSort); return asc(dbSort)
} else { } else {
return desc(dbSort); return desc(dbSort)
} }
}, },
offset: skip, offset: skip,
limit, limit,
}); })
return json(userCollectionItems); return json(userCollectionItems)
} catch (e) { } catch (e) {
console.error(e); console.error(e)
error(500, { message: 'Something went wrong' }); error(500, { message: 'Something went wrong' })
} }
} }

View file

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

View file

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

View file

@ -5,8 +5,8 @@
"scripts": { "scripts": {
"db:push": "drizzle-kit push", "db:push": "drizzle-kit push",
"db:generate": "drizzle-kit generate", "db:generate": "drizzle-kit generate",
"db:migrate": "tsx src/lib/server/api/infrastructure/database/migrate.ts", "db:migrate": "tsx src/lib/server/api/databases/migrate.ts",
"db:seed": "tsx src/lib/server/api/infrastructure/database/seed.ts", "db:seed": "tsx src/lib/server/api/databases/seed.ts",
"db:studio": "drizzle-kit studio --verbose", "db:studio": "drizzle-kit studio --verbose",
"dev": "NODE_OPTIONS=\"--inspect\" vite dev --host", "dev": "NODE_OPTIONS=\"--inspect\" vite dev --host",
"build": "vite build", "build": "vite build",

View file

@ -1,13 +1,13 @@
import { config } from 'dotenv'; import { config } from 'dotenv'
import { expand } from 'dotenv-expand'; import { expand } from 'dotenv-expand'
import { ZodError, z } from 'zod'; import { ZodError, z } from 'zod'
const stringBoolean = z.coerce const stringBoolean = z.coerce
.string() .string()
.transform((val) => { .transform((val) => {
return val === 'true'; return val === 'true'
}) })
.default('false'); .default('false')
const EnvSchema = z.object({ const EnvSchema = z.object({
ADMIN_USERNAME: z.string(), ADMIN_USERNAME: z.string(),
@ -20,6 +20,7 @@ const EnvSchema = z.object({
DB_MIGRATING: stringBoolean, DB_MIGRATING: stringBoolean,
DB_SEEDING: stringBoolean, DB_SEEDING: stringBoolean,
NODE_ENV: z.string().default('development'), NODE_ENV: z.string().default('development'),
ORIGIN: z.string(),
PUBLIC_SITE_NAME: z.string(), PUBLIC_SITE_NAME: z.string(),
PUBLIC_SITE_URL: z.string(), PUBLIC_SITE_URL: z.string(),
PUBLIC_UMAMI_DO_NOT_TRACK: z.string(), PUBLIC_UMAMI_DO_NOT_TRACK: z.string(),
@ -27,26 +28,25 @@ const EnvSchema = z.object({
PUBLIC_UMAMI_URL: z.string(), PUBLIC_UMAMI_URL: z.string(),
REDIS_URL: z.string(), REDIS_URL: z.string(),
TWO_FACTOR_TIMEOUT: z.coerce.number().default(300000), TWO_FACTOR_TIMEOUT: z.coerce.number().default(300000),
}); })
export type EnvSchema = z.infer<typeof EnvSchema>; export type EnvSchema = z.infer<typeof EnvSchema>
expand(config()); expand(config())
try { try {
EnvSchema.parse(process.env); EnvSchema.parse(process.env)
} catch (error) { } catch (error) {
if (error instanceof ZodError) { if (error instanceof ZodError) {
let message = 'Missing required values in .env:\n'; let message = 'Missing required values in .env:\n'
for (const issue of error.issues) { for (const issue of error.issues) {
message += `${issue.path[0]}\n`; message += `${issue.path[0]}\n`
} }
const e = new Error(message); const e = new Error(message)
e.stack = ''; e.stack = ''
throw e; throw e
} else {
console.error(error);
} }
console.error(error)
} }
export default EnvSchema.parse(process.env); export default EnvSchema.parse(process.env)

View file

@ -1,19 +1,10 @@
// import * as Sentry from '@sentry/sveltekit';
import 'reflect-metadata' import 'reflect-metadata'
import { hc } from 'hono/client'; import { StatusCodes } from '$lib/constants/status-codes'
import { sequence } from '@sveltejs/kit/hooks'; import type { ApiRoutes } from '$lib/server/api'
import { redirect, type Handle } from '@sveltejs/kit'; import { parseApiResponse } from '$lib/utils/api'
import type { ApiRoutes } from '$lib/server/api'; import { type Handle, redirect } from '@sveltejs/kit'
import { parseApiResponse } from '$lib/utils/api'; import { sequence } from '@sveltejs/kit/hooks'
import { StatusCodes } from '$lib/constants/status-codes'; import { hc } from 'hono/client'
// TODO: Fix Sentry as it is not working on SvelteKit v2
// Sentry.init({
// dsn: 'https://742e43279df93a3c4a4a78c12eb1f879@o4506057768632320.ingest.sentry.io/4506057770401792',
// tracesSampleRate: 1,
// environment: dev ? 'development' : 'production',
// enabled: !dev
// });
const apiClient: Handle = async ({ event, resolve }) => { const apiClient: Handle = async ({ event, resolve }) => {
/* ------------------------------ Register api ------------------------------ */ /* ------------------------------ Register api ------------------------------ */
@ -21,76 +12,31 @@ const apiClient: Handle = async ({ event, resolve }) => {
fetch: event.fetch, fetch: event.fetch,
headers: { headers: {
'x-forwarded-for': event.url.host.includes('sveltekit-prerender') ? '127.0.0.1' : event.getClientAddress(), 'x-forwarded-for': event.url.host.includes('sveltekit-prerender') ? '127.0.0.1' : event.getClientAddress(),
host: event.request.headers.get('host') || '' host: event.request.headers.get('host') || '',
} },
}); })
/* ----------------------------- Auth functions ----------------------------- */ /* ----------------------------- Auth functions ----------------------------- */
async function getAuthedUser() { async function getAuthedUser() {
const { data } = await api.user.$get().then(parseApiResponse) const { data } = await api.user.$get().then(parseApiResponse)
return data?.user; return data?.user
} }
async function getAuthedUserOrThrow() { async function getAuthedUserOrThrow() {
const { data } = await api.user.$get().then(parseApiResponse); const { data } = await api.user.$get().then(parseApiResponse)
if (!data || !data.user) throw redirect(StatusCodes.TEMPORARY_REDIRECT, '/'); if (!data || !data.user) throw redirect(StatusCodes.TEMPORARY_REDIRECT, '/')
return data?.user; return data?.user
} }
/* ------------------------------ Set contexts ------------------------------ */ /* ------------------------------ Set contexts ------------------------------ */
event.locals.api = api; event.locals.api = api
event.locals.parseApiResponse = parseApiResponse; event.locals.parseApiResponse = parseApiResponse
event.locals.getAuthedUser = getAuthedUser; event.locals.getAuthedUser = getAuthedUser
event.locals.getAuthedUserOrThrow = getAuthedUserOrThrow; event.locals.getAuthedUserOrThrow = getAuthedUserOrThrow
/* ----------------------------- Return response ---------------------------- */ /* ----------------------------- Return response ---------------------------- */
const response = await resolve(event); const response = await resolve(event)
return response; return response
}; }
// export const authentication: Handle = async function ({ event, resolve }) { export const handle: Handle = sequence(apiClient)
// event.locals.startTimer = Date.now();
//
// const ip = event.request.headers.get('x-forwarded-for') as string;
// const country = event.request.headers.get('x-vercel-ip-country') as string;
// event.locals.ip = dev ? '127.0.0.1' : ip; // || event.getClientAddress();
// event.locals.country = dev ? 'us' : country;
//
// const sessionId = event.cookies.get(lucia.sessionCookieName);
// if (!sessionId) {
// event.locals.user = null;
// event.locals.session = null;
// return resolve(event);
// }
//
// const { session, user } = await lucia.validateSession(sessionId);
// if (session && session.fresh) {
// const sessionCookie = lucia.createSessionCookie(session.id);
// console.log('sessionCookie', JSON.stringify(sessionCookie, null, 2));
// // sveltekit types deviates from the de-facto standard, you can use 'as any' too
// event.cookies.set(sessionCookie.name, sessionCookie.value, {
// path: '.',
// ...sessionCookie.attributes,
// });
// }
// console.log('session from hooks', JSON.stringify(session, null, 2));
// if (!session) {
// const sessionCookie = lucia.createBlankSessionCookie();
// console.log('blank sessionCookie', JSON.stringify(sessionCookie, null, 2));
// event.cookies.set(sessionCookie.name, sessionCookie.value, {
// path: '.',
// ...sessionCookie.attributes,
// });
// }
// event.locals.user = user;
// event.locals.session = session;
//
// return resolve(event);
// };
export const handle: Handle = sequence(
// Sentry.sentryHandle(),
// authentication,
apiClient
);
// export const handleError = Sentry.handleErrorWithSentry();

View file

@ -0,0 +1,8 @@
import { Hono } from 'hono'
import type { BlankSchema } from 'hono/types'
import type { HonoTypes } from '../../types'
export interface Controller {
controller: Hono<HonoTypes, BlankSchema, '/'>
routes(): any
}

View file

@ -0,0 +1,4 @@
export interface Email {
subject(): string
html(): string
}

View file

@ -0,0 +1,5 @@
import type { DatabaseProvider } from '$lib/server/api/providers/database.provider'
export interface Repository {
trxHost(trx: DatabaseProvider): any
}

View file

@ -0,0 +1,14 @@
import { HTTPException } from 'hono/http-exception'
export const takeFirst = <T>(values: T[]): T | null => {
if (values.length === 0) return null
return values[0] as T
}
export const takeFirstOrThrow = <T>(values: T[]): T => {
if (values.length === 0)
throw new HTTPException(404, {
message: 'Resource not found',
})
return values[0] as T
}

View file

@ -0,0 +1,29 @@
import { timestamp } from 'drizzle-orm/pg-core'
import { customType } from 'drizzle-orm/pg-core'
export const citext = customType<{ data: string }>({
dataType() {
return 'citext'
},
})
export const cuid2 = customType<{ data: string }>({
dataType() {
return 'text'
},
})
export const timestamps = {
createdAt: timestamp('created_at', {
mode: 'date',
withTimezone: true,
})
.notNull()
.defaultNow(),
updatedAt: timestamp('updated_at', {
mode: 'date',
withTimezone: true,
})
.notNull()
.defaultNow(),
}

View file

@ -1,31 +1,29 @@
import 'reflect-metadata'; import 'reflect-metadata'
import { Hono } from 'hono'; import type { Controller } from '$lib/server/api/common/interfaces/controller.interface'
import { inject, injectable } from 'tsyringe'; import { CollectionsService } from '$lib/server/api/services/collections.service'
import { requireAuth } from "../middleware/auth.middleware"; import { Hono } from 'hono'
import type { HonoTypes } from '../types'; import { inject, injectable } from 'tsyringe'
import type { Controller } from '../interfaces/controller.interface'; import { requireAuth } from '../middleware/auth.middleware'
import {CollectionsService} from "$lib/server/api/services/collections.service"; import type { HonoTypes } from '../types'
@injectable() @injectable()
export class CollectionController implements Controller { export class CollectionController implements Controller {
controller = new Hono<HonoTypes>(); controller = new Hono<HonoTypes>()
constructor( constructor(@inject(CollectionsService) private readonly collectionsService: CollectionsService) {}
@inject(CollectionsService) private readonly collectionsService: CollectionsService,
) { }
routes() { routes() {
return this.controller return this.controller
.get('/', requireAuth, async (c) => { .get('/', requireAuth, async (c) => {
const user = c.var.user; const user = c.var.user
const collections = await this.collectionsService.findAllByUserId(user.id); const collections = await this.collectionsService.findAllByUserId(user.id)
console.log('collections service', collections) console.log('collections service', collections)
return c.json({ collections }); return c.json({ collections })
}) })
.get('/:cuid', requireAuth, async (c) => { .get('/:cuid', requireAuth, async (c) => {
const cuid = c.req.param('cuid'); const cuid = c.req.param('cuid')
const collection = await this.collectionsService.findOneByCuid(cuid); const collection = await this.collectionsService.findOneByCuid(cuid)
return c.json({ collection }); return c.json({ collection })
}); })
} }
} }

View file

@ -1,17 +1,17 @@
import { Hono } from 'hono'
import { inject, injectable } from 'tsyringe'
import { setCookie } from 'hono/cookie'
import { zValidator } from '@hono/zod-validator'
import type { HonoTypes } from '../types'
import { requireAuth } from '../middleware/auth.middleware'
import type { Controller } from '$lib/server/api/interfaces/controller.interface'
import { IamService } from '$lib/server/api/services/iam.service'
import { LuciaProvider } from '$lib/server/api/providers'
import { limiter } from '$lib/server/api/middleware/rate-limiter.middleware'
import { updateProfileDto } from '$lib/dtos/update-profile.dto'
import { updateEmailDto } from '$lib/dtos/update-email.dto'
import { StatusCodes } from '$lib/constants/status-codes' import { StatusCodes } from '$lib/constants/status-codes'
import { verifyPasswordDto } from '$lib/dtos/verify-password.dto' import type { Controller } from '$lib/server/api/common/interfaces/controller.interface'
import { updateEmailDto } from '$lib/server/api/dtos/update-email.dto'
import { updateProfileDto } from '$lib/server/api/dtos/update-profile.dto'
import { verifyPasswordDto } from '$lib/server/api/dtos/verify-password.dto'
import { limiter } from '$lib/server/api/middleware/rate-limiter.middleware'
import { LuciaProvider } from '$lib/server/api/providers/lucia.provider'
import { IamService } from '$lib/server/api/services/iam.service'
import { zValidator } from '@hono/zod-validator'
import { Hono } from 'hono'
import { setCookie } from 'hono/cookie'
import { inject, injectable } from 'tsyringe'
import { requireAuth } from '../middleware/auth.middleware'
import type { HonoTypes } from '../types'
@injectable() @injectable()
export class IamController implements Controller { export class IamController implements Controller {

View file

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

View file

@ -1,15 +1,15 @@
import 'reflect-metadata' import 'reflect-metadata'
import { inject, injectable } from 'tsyringe'
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { requireAuth } from '../middleware/auth.middleware'
import type { HonoTypes } from '../types'
import type { Controller } from '$lib/server/api/interfaces/controller.interface'
import { TotpService } from '$lib/server/api/services/totp.service'
import { StatusCodes } from '$lib/constants/status-codes' import { StatusCodes } from '$lib/constants/status-codes'
import { verifyTotpDto } from '$lib/dtos/verify-totp.dto' import type { Controller } from '$lib/server/api/common/interfaces/controller.interface'
import { verifyTotpDto } from '$lib/server/api/dtos/verify-totp.dto'
import { TotpService } from '$lib/server/api/services/totp.service'
import { zValidator } from '@hono/zod-validator'
import { Hono } from 'hono'
import { inject, injectable } from 'tsyringe'
import { CredentialsType } from '../databases/tables'
import { requireAuth } from '../middleware/auth.middleware'
import { UsersService } from '../services/users.service' import { UsersService } from '../services/users.service'
import { CredentialsType } from '$db/tables' import type { HonoTypes } from '../types'
@injectable() @injectable()
export class MfaController implements Controller { export class MfaController implements Controller {

View file

@ -1,57 +1,58 @@
import 'reflect-metadata'; import 'reflect-metadata'
import { Hono } from 'hono'; import type { Controller } from '$lib/server/api/common/interfaces/controller.interface'
import { setCookie } from 'hono/cookie'; import { signupUsernameEmailDto } from '$lib/server/api/dtos/signup-username-email.dto'
import {inject, injectable} from 'tsyringe'; import { limiter } from '$lib/server/api/middleware/rate-limiter.middleware'
import { zValidator } from '@hono/zod-validator'; import { LuciaProvider } from '$lib/server/api/providers/lucia.provider'
import { TimeSpan } from 'oslo'; import { LoginRequestsService } from '$lib/server/api/services/loginrequest.service'
import type { HonoTypes } from '../types'; import { UsersService } from '$lib/server/api/services/users.service'
import type { Controller } from '../interfaces/controller.interface'; import { zValidator } from '@hono/zod-validator'
import { signupUsernameEmailDto } from "$lib/dtos/signup-username-email.dto"; import { Hono } from 'hono'
import {limiter} from "$lib/server/api/middleware/rate-limiter.middleware"; import { setCookie } from 'hono/cookie'
import {UsersService} from "$lib/server/api/services/users.service"; import { TimeSpan } from 'oslo'
import {LoginRequestsService} from "$lib/server/api/services/loginrequest.service"; import { inject, injectable } from 'tsyringe'
import {LuciaProvider} from "$lib/server/api/providers"; import type { HonoTypes } from '../types'
@injectable() @injectable()
export class SignupController implements Controller { export class SignupController implements Controller {
controller = new Hono<HonoTypes>(); controller = new Hono<HonoTypes>()
constructor( constructor(
@inject(UsersService) private readonly usersService: UsersService, @inject(UsersService) private readonly usersService: UsersService,
@inject(LoginRequestsService) private readonly loginRequestService: LoginRequestsService, @inject(LoginRequestsService) private readonly loginRequestService: LoginRequestsService,
@inject(LuciaProvider) private lucia: LuciaProvider @inject(LuciaProvider) private lucia: LuciaProvider,
) {} ) {}
routes() { routes() {
return this.controller return this.controller.post('/', zValidator('json', signupUsernameEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
.post('/', zValidator('json', signupUsernameEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => { const { firstName, lastName, email, username, password, confirm_password } = await c.req.valid('json')
const { firstName, lastName, email, username, password, confirm_password } = await c.req.valid('json'); const existingUser = await this.usersService.findOneByUsername(username)
const existingUser = await this.usersService.findOneByUsername(username);
if (existingUser) { if (existingUser) {
return c.body("User already exists", 400); return c.body('User already exists', 400)
} }
const user = await this.usersService.create({ firstName, lastName, email, username, password, confirm_password }); const user = await this.usersService.create({ firstName, lastName, email, username, password, confirm_password })
if (!user) { if (!user) {
return c.body("Failed to create user", 500); return c.body('Failed to create user', 500)
} }
const session = await this.loginRequestService.createUserSession(user.id, c.req, undefined); const session = await this.loginRequestService.createUserSession(user.id, c.req, undefined)
const sessionCookie = this.lucia.createSessionCookie(session.id); const sessionCookie = this.lucia.createSessionCookie(session.id)
console.log("set cookie", sessionCookie); console.log('set cookie', sessionCookie)
setCookie(c, sessionCookie.name, sessionCookie.value, { setCookie(c, sessionCookie.name, sessionCookie.value, {
path: sessionCookie.attributes.path, path: sessionCookie.attributes.path,
maxAge: sessionCookie?.attributes?.maxAge && sessionCookie?.attributes?.maxAge < new TimeSpan(365, 'd').seconds() maxAge:
? sessionCookie.attributes.maxAge : new TimeSpan(2, 'w').seconds(), sessionCookie?.attributes?.maxAge && sessionCookie?.attributes?.maxAge < new TimeSpan(365, 'd').seconds()
? sessionCookie.attributes.maxAge
: new TimeSpan(2, 'w').seconds(),
domain: sessionCookie.attributes.domain, domain: sessionCookie.attributes.domain,
sameSite: sessionCookie.attributes.sameSite as any, sameSite: sessionCookie.attributes.sameSite as any,
secure: sessionCookie.attributes.secure, secure: sessionCookie.attributes.secure,
httpOnly: sessionCookie.attributes.httpOnly, httpOnly: sessionCookie.attributes.httpOnly,
expires: sessionCookie.attributes.expires expires: sessionCookie.attributes.expires,
}); })
return c.json({ message: 'ok' }); return c.json({ message: 'ok' })
}); })
} }
} }

View file

@ -1,34 +1,32 @@
import 'reflect-metadata'; import 'reflect-metadata'
import { Hono } from 'hono'; import type { Controller } from '$lib/server/api/common/interfaces/controller.interface'
import { inject, injectable } from 'tsyringe'; import { UsersService } from '$lib/server/api/services/users.service'
import { requireAuth } from "../middleware/auth.middleware"; import { Hono } from 'hono'
import type { HonoTypes } from '../types'; import { inject, injectable } from 'tsyringe'
import type { Controller } from '../interfaces/controller.interface'; import { requireAuth } from '../middleware/auth.middleware'
import {UsersService} from "$lib/server/api/services/users.service"; import type { HonoTypes } from '../types'
@injectable() @injectable()
export class UserController implements Controller { export class UserController implements Controller {
controller = new Hono<HonoTypes>(); controller = new Hono<HonoTypes>()
constructor( constructor(@inject(UsersService) private readonly usersService: UsersService) {}
@inject(UsersService) private readonly usersService: UsersService
) { }
routes() { routes() {
return this.controller return this.controller
.get('/', async (c) => { .get('/', async (c) => {
const user = c.var.user; const user = c.var.user
return c.json({ user }); return c.json({ user })
}) })
.get('/:id', requireAuth, async (c) => { .get('/:id', requireAuth, async (c) => {
const id = c.req.param('id'); const id = c.req.param('id')
const user = await this.usersService.findOneById(id); const user = await this.usersService.findOneById(id)
return c.json({ user }); return c.json({ user })
}) })
.get('/username/:userName', requireAuth, async (c) => { .get('/username/:userName', requireAuth, async (c) => {
const userName = c.req.param('userName'); const userName = c.req.param('userName')
const user = await this.usersService.findOneByUsername(userName); const user = await this.usersService.findOneByUsername(userName)
return c.json({ user }); return c.json({ user })
}); })
} }
} }

View file

@ -1,30 +1,28 @@
import 'reflect-metadata'; import 'reflect-metadata'
import { Hono } from 'hono'; import type { Controller } from '$lib/server/api/common/interfaces/controller.interface'
import { inject, injectable } from 'tsyringe'; import { WishlistsService } from '$lib/server/api/services/wishlists.service'
import { requireAuth } from "../middleware/auth.middleware"; import { Hono } from 'hono'
import type { HonoTypes } from '../types'; import { inject, injectable } from 'tsyringe'
import type { Controller } from '../interfaces/controller.interface'; import { requireAuth } from '../middleware/auth.middleware'
import {WishlistsService} from "$lib/server/api/services/wishlists.service"; import type { HonoTypes } from '../types'
@injectable() @injectable()
export class WishlistController implements Controller { export class WishlistController implements Controller {
controller = new Hono<HonoTypes>(); controller = new Hono<HonoTypes>()
constructor( constructor(@inject(WishlistsService) private readonly wishlistsService: WishlistsService) {}
@inject(WishlistsService) private readonly wishlistsService: WishlistsService
) { }
routes() { routes() {
return this.controller return this.controller
.get('/', requireAuth, async (c) => { .get('/', requireAuth, async (c) => {
const user = c.var.user; const user = c.var.user
const wishlists = await this.wishlistsService.findAllByUserId(user.id); const wishlists = await this.wishlistsService.findAllByUserId(user.id)
return c.json({ wishlists }); return c.json({ wishlists })
}) })
.get('/:cuid', requireAuth, async (c) => { .get('/:cuid', requireAuth, async (c) => {
const cuid = c.req.param('cuid') const cuid = c.req.param('cuid')
const wishlist = await this.wishlistsService.findOneByCuid(cuid) const wishlist = await this.wishlistsService.findOneByCuid(cuid)
return c.json({ wishlist }); return c.json({ wishlist })
}); })
} }
} }

View file

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

View file

@ -1,11 +1,10 @@
import { collections } from '$lib/server/api/databases/tables'
import { createInsertSchema, createSelectSchema } from 'drizzle-zod' import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
import type { z } from 'zod' import type { z } from 'zod'
import { collections } from '$lib/server/api/infrastructure/database/tables'
export const InsertCollectionSchema = createInsertSchema(collections, { export const InsertCollectionSchema = createInsertSchema(collections, {
name: (schema) => schema.name.trim() name: (schema) =>
.min(3, { message: 'Must be at least 3 characters' }) schema.name.trim().min(3, { message: 'Must be at least 3 characters' }).max(64, { message: 'Must be less than 64 characters' }).optional(),
.max(64, { message: 'Must be less than 64 characters' }).optional(),
}).omit({ }).omit({
id: true, id: true,
cuid: true, cuid: true,

View file

@ -1,6 +1,6 @@
import { usersTable } from '$lib/server/api/databases/tables'
import { createInsertSchema, createSelectSchema } from 'drizzle-zod' import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
import type { z } from 'zod' import type { z } from 'zod'
import { usersTable } from '$lib/server/api/infrastructure/database/tables'
export const InsertUserSchema = createInsertSchema(usersTable, { export const InsertUserSchema = createInsertSchema(usersTable, {
email: (schema) => schema.email.max(64).email().optional(), email: (schema) => schema.email.max(64).email().optional(),

View file

@ -1,15 +1,15 @@
import { Table, getTableName, sql } from 'drizzle-orm'; import { Table, getTableName, sql } from 'drizzle-orm'
import env from '../../../../../env'; import env from '../../../../env'
import { db, pool } from './index'; import { db, pool } from '../packages/drizzle'
import * as schema from './tables'; import * as seeds from './seeds'
import * as seeds from './seeds'; import * as schema from './tables'
if (!env.DB_SEEDING) { if (!env.DB_SEEDING) {
throw new Error('You must set DB_SEEDING to "true" when running seeds'); throw new Error('You must set DB_SEEDING to "true" when running seeds')
} }
async function resetTable(db: db, table: Table) { async function resetTable(db: db, table: Table) {
return db.execute(sql.raw(`TRUNCATE TABLE ${getTableName(table)} RESTART IDENTITY CASCADE`)); return db.execute(sql.raw(`TRUNCATE TABLE ${getTableName(table)} RESTART IDENTITY CASCADE`))
} }
for (const table of [ for (const table of [
@ -41,11 +41,11 @@ for (const table of [
schema.wishlists, schema.wishlists,
]) { ]) {
// await db.delete(table); // clear tables without truncating / resetting ids // await db.delete(table); // clear tables without truncating / resetting ids
await resetTable(db, table); await resetTable(db, table)
} }
await seeds.roles(db); await seeds.roles(db)
await seeds.users(db); await seeds.users(db)
await pool.end(); await pool.end()
process.exit(); process.exit()

View file

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

View file

@ -1,31 +1,31 @@
import { eq } from 'drizzle-orm'; import * as schema from '$lib/server/api/databases/tables'
import { Argon2id } from 'oslo/password'; import { type db } from '$lib/server/api/packages/drizzle'
import { type db } from '$lib/server/api/infrastructure/database'; import { eq } from 'drizzle-orm'
import * as schema from '$lib/server/api/infrastructure/database/tables'; import { Argon2id } from 'oslo/password'
import users from './data/users.json'; import { config } from '../../configs/config'
import { config } from '../../../common/config'; import users from './data/users.json'
type JsonUser = { type JsonUser = {
id: string; id: string
username: string; username: string
email: string; email: string
password: string; password: string
roles: { roles: {
name: string; name: string
primary: boolean; primary: boolean
}[]; }[]
}; }
type JsonRole = { type JsonRole = {
name: string; name: string
primary: boolean; primary: boolean
}; }
export default async function seed(db: db) { export default async function seed(db: db) {
const adminRole = await db.select().from(schema.roles).where(eq(schema.roles.name, 'admin')); const adminRole = await db.select().from(schema.roles).where(eq(schema.roles.name, 'admin'))
const userRole = await db.select().from(schema.roles).where(eq(schema.roles.name, 'user')); const userRole = await db.select().from(schema.roles).where(eq(schema.roles.name, 'user'))
console.log('Admin Role: ', adminRole); console.log('Admin Role: ', adminRole)
const adminUser = await db const adminUser = await db
.insert(schema.usersTable) .insert(schema.usersTable)
.values({ .values({
@ -36,27 +36,19 @@ export default async function seed(db: db) {
verified: true, verified: true,
}) })
.returning() .returning()
.onConflictDoNothing(); .onConflictDoNothing()
console.log('Admin user created.', adminUser); console.log('Admin user created.', adminUser)
await db await db.insert(schema.credentialsTable).values({
.insert(schema.credentialsTable)
.values({
user_id: adminUser[0].id, user_id: adminUser[0].id,
type: schema.CredentialsType.PASSWORD, type: schema.CredentialsType.PASSWORD,
secret_data: await new Argon2id().hash(`${config.ADMIN_PASSWORD}`), secret_data: await new Argon2id().hash(`${config.ADMIN_PASSWORD}`),
}); })
await db await db.insert(schema.collections).values({ user_id: adminUser[0].id }).onConflictDoNothing()
.insert(schema.collections)
.values({ user_id: adminUser[0].id })
.onConflictDoNothing();
await db await db.insert(schema.wishlists).values({ user_id: adminUser[0].id }).onConflictDoNothing()
.insert(schema.wishlists)
.values({ user_id: adminUser[0].id })
.onConflictDoNothing();
await db await db
.insert(schema.user_roles) .insert(schema.user_roles)
@ -65,9 +57,9 @@ export default async function seed(db: db) {
role_id: adminRole[0].id, role_id: adminRole[0].id,
primary: true, primary: true,
}) })
.onConflictDoNothing(); .onConflictDoNothing()
console.log('Admin user given admin role.'); console.log('Admin user given admin role.')
await db await db
.insert(schema.user_roles) .insert(schema.user_roles)
@ -76,9 +68,9 @@ export default async function seed(db: db) {
role_id: userRole[0].id, role_id: userRole[0].id,
primary: false, primary: false,
}) })
.onConflictDoNothing(); .onConflictDoNothing()
console.log('Admin user given user role.'); console.log('Admin user given user role.')
await Promise.all( await Promise.all(
users.map(async (user) => { users.map(async (user) => {
const [insertedUser] = await db const [insertedUser] = await db
@ -86,27 +78,29 @@ export default async function seed(db: db) {
.values({ .values({
...user, ...user,
}) })
.returning(); .returning()
await db.insert(schema.credentialsTable).values({ await db.insert(schema.credentialsTable).values({
user_id: insertedUser?.id, user_id: insertedUser?.id,
type: schema.CredentialsType.PASSWORD, type: schema.CredentialsType.PASSWORD,
secret_data: await new Argon2id().hash(user.password), secret_data: await new Argon2id().hash(user.password),
}) })
await db.insert(schema.collections).values({ user_id: insertedUser?.id }); await db.insert(schema.collections).values({ user_id: insertedUser?.id })
await db.insert(schema.wishlists).values({ user_id: insertedUser?.id }); await db.insert(schema.wishlists).values({ user_id: insertedUser?.id })
await Promise.all( await Promise.all(
user.roles.map(async (role: JsonRole) => { user.roles.map(async (role: JsonRole) => {
const foundRole = await db.query.roles.findFirst({ const foundRole = await db.query.roles.findFirst({
where: eq(schema.roles.name, role.name), where: eq(schema.roles.name, role.name),
}); })
if (!foundRole) { throw new Error('Role not found'); }; if (!foundRole) {
throw new Error('Role not found')
}
await db.insert(schema.user_roles).values({ await db.insert(schema.user_roles).values({
user_id: insertedUser?.id, user_id: insertedUser?.id,
role_id: foundRole?.id, role_id: foundRole?.id,
primary: role?.primary, primary: role?.primary,
}); })
}), }),
); )
}), }),
); )
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
import { pgTable, text, uuid } from "drizzle-orm/pg-core"; import { timestamps } from '$lib/server/api/common/utils/table.utils'
import { type InferSelectModel } from 'drizzle-orm'; import { type InferSelectModel } from 'drizzle-orm'
import { usersTable } from "./users.table"; import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../utils'; import { usersTable } from './users.table'
export const federatedIdentityTable = pgTable('federated_identity', { export const federatedIdentityTable = pgTable('federated_identity', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid('id').primaryKey().defaultRandom(),
@ -11,7 +11,7 @@ export const federatedIdentityTable = pgTable('federated_identity', {
identity_provider: text('identity_provider').notNull(), identity_provider: text('identity_provider').notNull(),
federated_user_id: text('federated_user_id').notNull(), federated_user_id: text('federated_user_id').notNull(),
federated_username: text('federated_username').notNull(), federated_username: text('federated_username').notNull(),
...timestamps ...timestamps,
}); })
export type FederatedIdentity = InferSelectModel<typeof federatedIdentityTable>; export type FederatedIdentity = InferSelectModel<typeof federatedIdentityTable>

View file

@ -1,11 +1,11 @@
import { index, integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'; import { timestamps } from '$lib/server/api/common/utils/table.utils'
import { createId as cuid2 } from '@paralleldrive/cuid2'; import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations, sql } from 'drizzle-orm'; import { type InferSelectModel, relations, sql } from 'drizzle-orm'
import {categories_to_games_table} from './categoriesToGames'; import { index, integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
import {gamesToExternalIds} from './gamesToExternalIds'; import { categories_to_games_table } from './categoriesToGames'
import {mechanics_to_games} from './mechanicsToGames'; import { gamesToExternalIds } from './gamesToExternalIds'
import {publishers_to_games} from './publishersToGames'; import { mechanics_to_games } from './mechanicsToGames'
import { timestamps } from '../utils'; import { publishers_to_games } from './publishersToGames'
export const games = pgTable( export const games = pgTable(
'games', 'games',
@ -39,13 +39,13 @@ export const games = pgTable(
)`, )`,
), ),
}), }),
); )
export const gameRelations = relations(games, ({ many }) => ({ export const gameRelations = relations(games, ({ many }) => ({
categories_to_games: many(categories_to_games_table), categories_to_games: many(categories_to_games_table),
mechanics_to_games: many(mechanics_to_games), mechanics_to_games: many(mechanics_to_games),
publishers_to_games: many(publishers_to_games), publishers_to_games: many(publishers_to_games),
gamesToExternalIds: many(gamesToExternalIds), gamesToExternalIds: many(gamesToExternalIds),
})); }))
export type Games = InferSelectModel<typeof games>; export type Games = InferSelectModel<typeof games>

View file

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

View file

@ -0,0 +1,23 @@
import { timestamps } from '$lib/server/api/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,7 +1,7 @@
import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core'; import { timestamps } from '$lib/server/api/common/utils/table.utils'
import type { InferSelectModel } from 'drizzle-orm'; import type { InferSelectModel } from 'drizzle-orm'
import { usersTable } from './users.table'; import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { timestamps } from '../utils'; import { usersTable } from './users.table'
export const recoveryCodesTable = pgTable('recovery_codes', { export const recoveryCodesTable = pgTable('recovery_codes', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid('id').primaryKey().defaultRandom(),
@ -11,6 +11,6 @@ export const recoveryCodesTable = pgTable('recovery_codes', {
code: text('code').notNull(), code: text('code').notNull(),
used: boolean('used').default(false), used: boolean('used').default(false),
...timestamps, ...timestamps,
}); })
export type RecoveryCodesTable = InferSelectModel<typeof recoveryCodesTable>; export type RecoveryCodesTable = InferSelectModel<typeof recoveryCodesTable>

View file

@ -0,0 +1,21 @@
import { timestamps } from '$lib/server/api/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 { user_roles } from './userRoles'
export const roles = pgTable('roles', {
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
.unique()
.$defaultFn(() => cuid2())
.notNull(),
name: text('name').unique().notNull(),
...timestamps,
})
export type Roles = InferSelectModel<typeof roles>
export const role_relations = relations(roles, ({ many }) => ({
user_roles: many(user_roles),
}))

View file

@ -1,8 +1,8 @@
import { createId as cuid2 } from '@paralleldrive/cuid2'; import { timestamps } from '$lib/server/api/common/utils/table.utils'
import { type InferSelectModel, relations } from 'drizzle-orm'; import { createId as cuid2 } from '@paralleldrive/cuid2'
import { boolean, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'; import { type InferSelectModel, relations } from 'drizzle-orm'
import { timestamps } from '../utils'; import { boolean, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
import { usersTable } from './users.table'; import { usersTable } from './users.table'
export const twoFactorTable = pgTable('two_factor', { export const twoFactorTable = pgTable('two_factor', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid('id').primaryKey().defaultRandom(),
@ -20,13 +20,13 @@ export const twoFactorTable = pgTable('two_factor', {
.references(() => usersTable.id) .references(() => usersTable.id)
.unique(), .unique(),
...timestamps, ...timestamps,
}); })
export const emailVerificationsRelations = relations(twoFactorTable, ({ one }) => ({ export const emailVerificationsRelations = relations(twoFactorTable, ({ one }) => ({
user: one(usersTable, { user: one(usersTable, {
fields: [twoFactorTable.userId], fields: [twoFactorTable.userId],
references: [usersTable.id], references: [usersTable.id],
}), }),
})); }))
export type TwoFactor = InferSelectModel<typeof twoFactorTable>; export type TwoFactor = InferSelectModel<typeof twoFactorTable>

View file

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

View file

@ -1,7 +1,7 @@
import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core' import { timestamps } from '$lib/server/api/common/utils/table.utils'
import { type InferSelectModel, relations } from 'drizzle-orm'
import { createId as cuid2 } from '@paralleldrive/cuid2' import { createId as cuid2 } from '@paralleldrive/cuid2'
import { timestamps } from '../utils' import { type InferSelectModel, relations } from 'drizzle-orm'
import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { user_roles } from './userRoles' import { user_roles } from './userRoles'
export const usersTable = pgTable('users', { export const usersTable = pgTable('users', {

View file

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

View file

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

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,5 @@
import { z } from "zod";
export const IdParamsDto = z.object({
id: z.trim().number(),
});

View file

@ -0,0 +1,20 @@
import { z } from "zod";
import { refinePasswords } from "$lib/validations/account";
export const registerEmailPasswordDto = z.object({
firstName: z.string().trim().optional(),
lastName: z.string().trim().optional(),
email: z.string().trim().max(64, { message: 'Email must be less than 64 characters' }).optional(),
username: z
.string()
.trim()
.min(3, { message: 'Must be at least 3 characters' })
.max(50, { message: 'Must be less than 50 characters' }),
password: z.string({ required_error: 'Password is required' }).trim(),
confirm_password: z.string({ required_error: 'Confirm Password is required' }).trim(),
})
.superRefine(({ confirm_password, password }, ctx) => {
refinePasswords(confirm_password, password, ctx);
});
export type RegisterEmailPasswordDto = z.infer<typeof registerEmailPasswordDto>;

View file

@ -0,0 +1,12 @@
import { z } from "zod";
export const signinUsernameDto = z.object({
username: z
.string()
.trim()
.min(3, { message: 'Must be at least 3 characters' })
.max(50, { message: 'Must be less than 50 characters' }),
password: z.string({ required_error: 'Password is required' }).trim(),
});
export type SigninUsernameDto = z.infer<typeof signinUsernameDto>;

View file

@ -0,0 +1,24 @@
import { z } from "zod";
import { refinePasswords } from "$lib/validations/account";
export const signupUsernameEmailDto = z.object({
firstName: z.string().trim().optional(),
lastName: z.string().trim().optional(),
email: z.string()
.trim()
.max(64, {message: 'Email must be less than 64 characters'})
.email({message: 'Please enter a valid email'})
.optional(),
username: z
.string()
.trim()
.min(3, {message: 'Must be at least 3 characters'})
.max(50, {message: 'Must be less than 50 characters'}),
password: z.string({required_error: 'Password is required'}).trim(),
confirm_password: z.string({required_error: 'Confirm Password is required'}).trim()
})
.superRefine(({ confirm_password, password }, ctx) => {
return refinePasswords(confirm_password, password, ctx);
});
export type SignupUsernameEmailDto = z.infer<typeof signupUsernameEmailDto>

View file

@ -0,0 +1,11 @@
import { z } from "zod";
export const updateEmailDto = z.object({
email: z
.string()
.trim()
.max(64, {message: 'Email must be less than 64 characters'})
.email({message: 'Please enter a valid email'})
});
export type UpdateEmailDto = z.infer<typeof updateEmailDto>;

View file

@ -0,0 +1,23 @@
import { z } from "zod";
export const updateProfileDto = z.object({
firstName: z
.string()
.trim()
.min(3, {message: 'Must be at least 3 characters'})
.max(50, {message: 'Must be less than 50 characters'})
.optional(),
lastName: z
.string()
.trim()
.min(3, {message: 'Must be at least 3 characters'})
.max(50, {message: 'Must be less than 50 characters'})
.optional(),
username: z
.string()
.trim()
.min(3, {message: 'Must be at least 3 characters'})
.max(50, {message: 'Must be less than 50 characters'})
});
export type UpdateProfileDto = z.infer<typeof updateProfileDto>;

View file

@ -0,0 +1,7 @@
import { z } from 'zod'
export const verifyPasswordDto = z.object({
password: z.string({ required_error: 'Password is required' }).trim(),
})
export type VerifyPasswordDto = z.infer<typeof verifyPasswordDto>

View file

@ -0,0 +1,11 @@
import { z } from "zod";
export const verifyTotpDto = z.object({
code: z
.string()
.trim()
.min(6, { message: 'Must be at least 6 characters' })
.max(6, { message: 'Must be less than 6 characters' }),
});
export type VerifyTotpDto = z.infer<typeof verifyTotpDto>;

View file

@ -1,43 +1,45 @@
import 'reflect-metadata' import 'reflect-metadata'
import { Hono } from 'hono'; import { CollectionController } from '$lib/server/api/controllers/collection.controller'
import { hc } from 'hono/client'; import { MfaController } from '$lib/server/api/controllers/mfa.controller'
import { cors } from 'hono/cors'; import { SignupController } from '$lib/server/api/controllers/signup.controller'
import { logger } from 'hono/logger'; import { UserController } from '$lib/server/api/controllers/user.controller'
import { validateAuthSession, verifyOrigin } from './middleware/auth.middleware'; import { WishlistController } from '$lib/server/api/controllers/wishlist.controller'
import { config } from './common/config'; import { AuthCleanupJobs } from '$lib/server/api/jobs/auth-cleanup.job'
import { container } from 'tsyringe'; import { Hono } from 'hono'
import { IamController } from './controllers/iam.controller'; import { hc } from 'hono/client'
import { LoginController } from './controllers/login.controller'; import { cors } from 'hono/cors'
import { MfaController} from "$lib/server/api/controllers/mfa.controller"; import { logger } from 'hono/logger'
import {UserController} from "$lib/server/api/controllers/user.controller"; import { container } from 'tsyringe'
import {SignupController} from "$lib/server/api/controllers/signup.controller"; import { config } from './configs/config'
import {WishlistController} from "$lib/server/api/controllers/wishlist.controller"; import { IamController } from './controllers/iam.controller'
import {CollectionController} from "$lib/server/api/controllers/collection.controller"; import { LoginController } from './controllers/login.controller'
import { validateAuthSession, verifyOrigin } from './middleware/auth.middleware'
/* ----------------------------------- Api ---------------------------------- */ /* -------------------------------------------------------------------------- */
const app = new Hono().basePath('/api'); /* App */
/* -------------------------------------------------------------------------- */
export const app = new Hono().basePath('/api')
/* --------------------------- Global Middlewares --------------------------- */ /* -------------------------------------------------------------------------- */
app.use(verifyOrigin).use(validateAuthSession); /* Global Middlewares */
app.use(logger()); /* -------------------------------------------------------------------------- */
app.use(verifyOrigin).use(validateAuthSession)
app.use(logger())
app.use( app.use(
'/*', '/*',
cors({ cors({
origin: [ origin: ['http://localhost:5173', 'http://localhost:80', 'http://host.docker.internal:80', 'http://host.docker.internal:5173'], // Replace with your allowed domains
'http://localhost:5173',
'http://localhost:80',
'http://host.docker.internal:80',
'http://host.docker.internal:5173'
], // Replace with your allowed domains
allowMethods: ['POST'], allowMethods: ['POST'],
allowHeaders: ['Content-Type'] allowHeaders: ['Content-Type'],
// credentials: true, // If you need to send cookies or HTTP authentication // credentials: true, // If you need to send cookies or HTTP authentication
}) }),
); )
/* --------------------------------- Routes --------------------------------- */ /* -------------------------------------------------------------------------- */
/* Routes */
/* -------------------------------------------------------------------------- */
const routes = app const routes = app
.route('/me', container.resolve(IamController).routes()) .route('/me', container.resolve(IamController).routes())
.route('/user', container.resolve(UserController).routes()) .route('/user', container.resolve(UserController).routes())
@ -46,14 +48,17 @@ const routes = app
.route('/wishlists', container.resolve(WishlistController).routes()) .route('/wishlists', container.resolve(WishlistController).routes())
.route('/collections', container.resolve(CollectionController).routes()) .route('/collections', container.resolve(CollectionController).routes())
.route('/mfa', container.resolve(MfaController).routes()) .route('/mfa', container.resolve(MfaController).routes())
.get('/', (c) => c.json({ message: 'Server is healthy' })); .get('/', (c) => c.json({ message: 'Server is healthy' }))
/* -------------------------------------------------------------------------- */
/* Cron Jobs */
/* -------------------------------------------------------------------------- */
container.resolve(AuthCleanupJobs).deleteStaleEmailVerificationRequests()
container.resolve(AuthCleanupJobs).deleteStaleLoginRequests()
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* Exports */ /* Exports */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
export type AppType = typeof routes; export const rpc = hc<typeof routes>(config.ORIGIN)
export type ApiClient = typeof rpc
export const rpc = hc<AppType>(config.ORIGIN); export type ApiRoutes = typeof routes
export type ApiClient = typeof rpc;
export type ApiRoutes = typeof routes;
export { app };

View file

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

View file

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

View file

@ -1,23 +0,0 @@
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import {mechanics_to_games} from './mechanicsToGames';
import {mechanicsToExternalIds} from './mechanicsToExternalIds';
import { timestamps } from '../utils';
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 { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import {publishers_to_games} from './publishersToGames';
import {publishersToExternalIds} from './publishersToExternalIds';
import { timestamps } from '../utils';
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,21 +0,0 @@
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import {user_roles} from './userRoles';
import { timestamps } from '../utils';
export const roles = pgTable('roles', {
id: uuid('id').primaryKey().defaultRandom(),
cuid: text('cuid')
.unique()
.$defaultFn(() => cuid2())
.notNull(),
name: text('name').unique().notNull(),
...timestamps,
});
export type Roles = InferSelectModel<typeof roles>;
export const role_relations = relations(roles, ({ many }) => ({
user_roles: many(user_roles),
}));

View file

@ -1,42 +0,0 @@
import { HTTPException } from 'hono/http-exception';
import { timestamp, customType } from 'drizzle-orm/pg-core';
export const citext = customType<{ data: string }>({
dataType() {
return 'citext';
}
});
export const cuid2 = customType<{ data: string }>({
dataType() {
return 'text';
}
});
export const takeFirst = <T>(values: T[]): T | null => {
if (values.length === 0) return null;
return values[0]!;
};
export const takeFirstOrThrow = <T>(values: T[]): T => {
if (values.length === 0)
throw new HTTPException(404, {
message: 'Resource not found'
});
return values[0]!;
};
export const timestamps = {
createdAt: timestamp('created_at', {
mode: 'date',
withTimezone: true
})
.notNull()
.defaultNow(),
updatedAt: timestamp('updated_at', {
mode: 'date',
withTimezone: true
})
.notNull()
.defaultNow()
};

View file

@ -1,8 +0,0 @@
import { Hono } from 'hono';
import type { HonoTypes } from '../types';
import type { BlankSchema } from 'hono/types';
export interface Controller {
controller: Hono<HonoTypes, BlankSchema, '/'>;
routes(): any;
}

View file

@ -0,0 +1,42 @@
import { inject, injectable } from 'tsyringe'
import { JobsService } from '../services/jobs.service'
@injectable()
export class AuthCleanupJobs {
private queue
constructor(@inject(JobsService) private jobsService: JobsService) {
/* ------------------------------ Create Queue ------------------------------ */
this.queue = this.jobsService.createQueue('test')
/* ---------------------------- Register Workers ---------------------------- */
this.worker().then((r) => console.log('auth-cleanup job worker started'))
}
async deleteStaleEmailVerificationRequests() {
await this.queue.add('delete_stale_email_verifiactions', null, {
repeat: {
pattern: '0 0 * * 0', // Runs once a week at midnight on Sunday
},
})
}
async deleteStaleLoginRequests() {
await this.queue.add('delete_stale_login_requests', null, {
repeat: {
pattern: '0 0 * * 0', // Runs once a week at midnight on Sunday
},
})
}
private async worker() {
return this.jobsService.createWorker(this.queue.name, async (job) => {
if (job.name === 'delete_stale_email_verifiactions') {
// delete stale email verifications
}
if (job.name === 'delete_stale_login_requests') {
// delete stale email verifications
}
})
}
}

View file

@ -1,10 +1,10 @@
import type { MiddlewareHandler } from 'hono' import type { MiddlewareHandler } from 'hono'
import { createMiddleware } from 'hono/factory' import { createMiddleware } from 'hono/factory'
import type { HonoTypes } from '../types'
import { lucia } from '../infrastructure/auth/lucia'
import { verifyRequestOrigin } from 'oslo/request'
import type { Session, User } from 'lucia' import type { Session, User } from 'lucia'
import { Unauthorized } from '../common/errors' import { verifyRequestOrigin } from 'oslo/request'
import { Unauthorized } from '../common/exceptions'
import { lucia } from '../packages/lucia'
import type { HonoTypes } from '../types'
export const verifyOrigin: MiddlewareHandler<HonoTypes> = createMiddleware(async (c, next) => { export const verifyOrigin: MiddlewareHandler<HonoTypes> = createMiddleware(async (c, next) => {
if (c.req.method === 'GET') { if (c.req.method === 'GET') {

View file

@ -1,24 +1,28 @@
import { rateLimiter } from "hono-rate-limiter"; import { rateLimiter } from 'hono-rate-limiter'
import { RedisStore } from 'rate-limit-redis'
import RedisClient from 'ioredis' import RedisClient from 'ioredis'
import type { HonoTypes } from "../types"; import { RedisStore } from 'rate-limit-redis'
import { config } from "../common/config"; import { config } from '../configs/config'
import type { HonoTypes } from '../types'
const client = new RedisClient(config.REDIS_URL) const client = new RedisClient(config.REDIS_URL)
export function limiter({ limit, minutes, key = "" }: { export function limiter({
limit: number; limit,
minutes: number; minutes,
key?: string; key = '',
}: {
limit: number
minutes: number
key?: string
}) { }) {
return rateLimiter({ return rateLimiter({
windowMs: minutes * 60 * 1000, // every x minutes windowMs: minutes * 60 * 1000, // every x minutes
limit, // Limit each IP to 100 requests per `window` (here, per 15 minutes). limit, // Limit each IP to 100 requests per `window` (here, per 15 minutes).
standardHeaders: "draft-6", // draft-6: `RateLimit-*` headers; draft-7: combined `RateLimit` header standardHeaders: 'draft-6', // draft-6: `RateLimit-*` headers; draft-7: combined `RateLimit` header
keyGenerator: (c) => { keyGenerator: (c) => {
const vars = c.var as HonoTypes['Variables']; const vars = c.var as HonoTypes['Variables']
const clientKey = vars.user?.id || c.req.header("x-forwarded-for"); const clientKey = vars.user?.id || c.req.header('x-forwarded-for')
const pathKey = key || c.req.routePath; const pathKey = key || c.req.routePath
return `${clientKey}_${pathKey}` return `${clientKey}_${pathKey}`
}, // Method to generate custom identifiers for clients. }, // Method to generate custom identifiers for clients.
// Redis store configuration // Redis store configuration
@ -28,5 +32,3 @@ export function limiter({ limit, minutes, key = "" }: {
}) as any, }) as any,
}) })
} }

View file

@ -1,7 +1,7 @@
import { drizzle } from 'drizzle-orm/node-postgres'; import { drizzle } from 'drizzle-orm/node-postgres'
import pg from 'pg'; import pg from 'pg'
import { config } from '../../common/config'; import { config } from '../configs/config'
import * as schema from './tables'; import * as schema from '../databases/tables'
// create the connection // create the connection
export const pool = new pg.Pool({ export const pool = new pg.Pool({
@ -12,11 +12,11 @@ export const pool = new pg.Pool({
database: config.DATABASE_DB, database: config.DATABASE_DB,
ssl: config.DATABASE_HOST !== 'localhost', ssl: config.DATABASE_HOST !== 'localhost',
max: config.DB_MIGRATING || config.DB_SEEDING ? 1 : undefined, max: config.DB_MIGRATING || config.DB_SEEDING ? 1 : undefined,
}); })
export const db = drizzle(pool, { export const db = drizzle(pool, {
schema, schema,
logger: config.NODE_ENV === 'development', logger: config.NODE_ENV === 'development',
}); })
export type db = typeof db; export type db = typeof db

View file

@ -1,11 +1,11 @@
import { DrizzlePostgreSQLAdapter } from '@lucia-auth/adapter-drizzle'
// lib/server/lucia.ts // lib/server/lucia.ts
import { Lucia, TimeSpan } from 'lucia'; import { Lucia, TimeSpan } from 'lucia'
import { DrizzlePostgreSQLAdapter } from '@lucia-auth/adapter-drizzle'; import { config } from '../configs/config'
import { db } from '../database'; import { sessionsTable, usersTable } from '../databases/tables'
import { sessionsTable, usersTable } from '../database/tables'; import { db } from './drizzle'
import { config } from '../../common/config';
const adapter = new DrizzlePostgreSQLAdapter(db, sessionsTable, usersTable); const adapter = new DrizzlePostgreSQLAdapter(db, sessionsTable, usersTable)
export const lucia = new Lucia(adapter, { export const lucia = new Lucia(adapter, {
getSessionAttributes: (attributes) => { getSessionAttributes: (attributes) => {
@ -14,7 +14,7 @@ export const lucia = new Lucia(adapter, {
ipAddress: attributes.ip_address, ipAddress: attributes.ip_address,
isTwoFactorAuthEnabled: attributes.twoFactorAuthEnabled, isTwoFactorAuthEnabled: attributes.twoFactorAuthEnabled,
isTwoFactorAuthenticated: attributes.isTwoFactorAuthenticated, isTwoFactorAuthenticated: attributes.isTwoFactorAuthenticated,
}; }
}, },
getUserAttributes: (attributes) => { getUserAttributes: (attributes) => {
return { return {
@ -25,7 +25,7 @@ export const lucia = new Lucia(adapter, {
lastName: attributes.last_name, lastName: attributes.last_name,
mfa_enabled: attributes.mfa_enabled, mfa_enabled: attributes.mfa_enabled,
theme: attributes.theme, theme: attributes.theme,
}; }
}, },
sessionExpiresIn: new TimeSpan(2, 'w'), // 2 weeks sessionExpiresIn: new TimeSpan(2, 'w'), // 2 weeks
sessionCookie: { sessionCookie: {
@ -38,26 +38,26 @@ export const lucia = new Lucia(adapter, {
domain: config.domain, domain: config.domain,
}, },
}, },
}); })
declare module 'lucia' { declare module 'lucia' {
interface Register { interface Register {
Lucia: typeof lucia; Lucia: typeof lucia
DatabaseUserAttributes: DatabaseUserAttributes; DatabaseUserAttributes: DatabaseUserAttributes
DatabaseSessionAttributes: DatabaseSessionAttributes; DatabaseSessionAttributes: DatabaseSessionAttributes
} }
interface DatabaseSessionAttributes { interface DatabaseSessionAttributes {
ip_country: string; ip_country: string
ip_address: string; ip_address: string
twoFactorAuthEnabled: boolean; twoFactorAuthEnabled: boolean
isTwoFactorAuthenticated: boolean; isTwoFactorAuthenticated: boolean
} }
interface DatabaseUserAttributes { interface DatabaseUserAttributes {
username: string; username: string
email: string; email: string
first_name: string; first_name: string
last_name: string; last_name: string
mfa_enabled: boolean; mfa_enabled: boolean
theme: string; theme: string
} }
} }

View file

@ -1,11 +1,11 @@
import { container } from 'tsyringe'; import { container } from 'tsyringe'
import { db } from '../infrastructure/database'; import { db } from '../packages/drizzle'
// Symbol // Symbol
export const DatabaseProvider = Symbol('DATABASE_TOKEN'); export const DatabaseProvider = Symbol('DATABASE_TOKEN')
// Type // Type
export type DatabaseProvider = typeof db; export type DatabaseProvider = typeof db
// Register // Register
container.register<DatabaseProvider>(DatabaseProvider, { useValue: db }); container.register<DatabaseProvider>(DatabaseProvider, { useValue: db })

View file

@ -1,3 +0,0 @@
export * from './database.provider';
export * from './lucia.provider';
export * from './redis.provider';

View file

@ -1,11 +1,11 @@
import { container } from 'tsyringe'; import { container } from 'tsyringe'
import { lucia } from '../infrastructure/auth/lucia'; import { lucia } from '../packages/lucia'
// Symbol // Symbol
export const LuciaProvider = Symbol('LUCIA_PROVIDER'); export const LuciaProvider = Symbol('LUCIA_PROVIDER')
// Type // Type
export type LuciaProvider = typeof lucia; export type LuciaProvider = typeof lucia
// Register // Register
container.register<LuciaProvider>(LuciaProvider, { useValue: lucia }); container.register<LuciaProvider>(LuciaProvider, { useValue: lucia })

View file

@ -1,14 +1,11 @@
import { container } from 'tsyringe';
import RedisClient from 'ioredis' import RedisClient from 'ioredis'
import { config } from '../common/config'; import { container } from 'tsyringe'
import { config } from '../configs/config'
// Symbol export const RedisProvider = Symbol('REDIS_TOKEN')
export const RedisProvider = Symbol('REDIS_TOKEN'); export type RedisProvider = RedisClient
// Type
export type RedisProvider = RedisClient;
// Register
container.register<RedisProvider>(RedisProvider, { container.register<RedisProvider>(RedisProvider, {
useValue: new RedisClient(config.REDIS_URL) useValue: new RedisClient(config.REDIS_URL, {
}); maxRetriesPerRequest: null,
}),
})

View file

@ -1,18 +1,19 @@
import {inject, injectable} from "tsyringe"; import type { Repository } from '$lib/server/api/common/interfaces/repository.interface'
import { eq, type InferInsertModel } from "drizzle-orm"; import { takeFirstOrThrow } from '$lib/server/api/common/utils/repository.utils'
import {DatabaseProvider} from "$lib/server/api/providers"; import { DatabaseProvider } from '$lib/server/api/providers/database.provider'
import { collections } from "../infrastructure/database/tables"; import { type InferInsertModel, eq } from 'drizzle-orm'
import { takeFirstOrThrow } from "../infrastructure/database/utils"; import { inject, injectable } from 'tsyringe'
import { collections } from '../databases/tables'
export type CreateCollection = InferInsertModel<typeof collections>; export type CreateCollection = InferInsertModel<typeof collections>
export type UpdateCollection = Partial<CreateCollection>; export type UpdateCollection = Partial<CreateCollection>
@injectable() @injectable()
export class CollectionsRepository { export class CollectionsRepository implements Repository {
constructor(@inject(DatabaseProvider) private readonly db: DatabaseProvider) {} constructor(@inject(DatabaseProvider) private readonly db: DatabaseProvider) {}
async findAll() { async findAll() {
return this.db.query.collections.findMany(); return this.db.query.collections.findMany()
} }
async findOneById(id: string) { async findOneById(id: string) {
@ -20,8 +21,8 @@ export class CollectionsRepository {
where: eq(collections.id, id), where: eq(collections.id, id),
columns: { columns: {
cuid: true, cuid: true,
name: true name: true,
} },
}) })
} }
@ -30,8 +31,8 @@ export class CollectionsRepository {
where: eq(collections.cuid, cuid), where: eq(collections.cuid, cuid),
columns: { columns: {
cuid: true, cuid: true,
name: true name: true,
} },
}) })
} }
@ -40,27 +41,26 @@ export class CollectionsRepository {
where: eq(collections.user_id, userId), where: eq(collections.user_id, userId),
columns: { columns: {
cuid: true, cuid: true,
name: true name: true,
} },
}) })
} }
async findAllByUserId(userId: string) { async findAllByUserId(userId: string) {
return this.db.query.collections.findMany({ return this.db.query.collections.findMany({
where: eq(collections.user_id, userId) where: eq(collections.user_id, userId),
}) })
} }
async create(data: CreateCollection) { async create(data: CreateCollection) {
return this.db.insert(collections).values(data).returning().then(takeFirstOrThrow); return this.db.insert(collections).values(data).returning().then(takeFirstOrThrow)
} }
async update(id: string, data: UpdateCollection) { async update(id: string, data: UpdateCollection) {
return this.db return this.db.update(collections).set(data).where(eq(collections.id, id)).returning().then(takeFirstOrThrow)
.update(collections) }
.set(data)
.where(eq(collections.id, id)) trxHost(trx: DatabaseProvider) {
.returning() return new CollectionsRepository(trx)
.then(takeFirstOrThrow);
} }
} }

View file

@ -1,14 +1,15 @@
import { and, eq, type InferInsertModel } from 'drizzle-orm' import type { Repository } from '$lib/server/api/common/interfaces/repository.interface'
import { credentialsTable, CredentialsType } from '../infrastructure/database/tables/credentials.table' import { CredentialsType, credentialsTable } from '$lib/server/api/databases/tables/credentials.table'
import { takeFirstOrThrow } from '../infrastructure/database/utils' import { DatabaseProvider } from '$lib/server/api/providers/database.provider'
import { type InferInsertModel, and, eq } from 'drizzle-orm'
import { inject, injectable } from 'tsyringe' import { inject, injectable } from 'tsyringe'
import { DatabaseProvider } from '$lib/server/api/providers' import { takeFirstOrThrow } from '../common/utils/repository.utils'
export type CreateCredentials = InferInsertModel<typeof credentialsTable> export type CreateCredentials = InferInsertModel<typeof credentialsTable>
export type UpdateCredentials = Partial<CreateCredentials> export type UpdateCredentials = Partial<CreateCredentials>
@injectable() @injectable()
export class CredentialsRepository { export class CredentialsRepository implements Repository {
constructor(@inject(DatabaseProvider) private readonly db: DatabaseProvider) {} constructor(@inject(DatabaseProvider) private readonly db: DatabaseProvider) {}
async findOneByUserId(userId: string) { async findOneByUserId(userId: string) {
@ -66,4 +67,8 @@ export class CredentialsRepository {
async deleteByUserIdAndType(userId: string, type: CredentialsType) { async deleteByUserIdAndType(userId: string, type: CredentialsType) {
return this.db.delete(credentialsTable).where(and(eq(credentialsTable.user_id, userId), eq(credentialsTable.type, type))) return this.db.delete(credentialsTable).where(and(eq(credentialsTable.user_id, userId), eq(credentialsTable.type, type)))
} }
trxHost(trx: DatabaseProvider) {
return new CredentialsRepository(trx)
}
} }

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