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.*
*.xdp*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
.vercel
.output
.idea

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,13 +1,13 @@
import { config } from 'dotenv';
import { expand } from 'dotenv-expand';
import { ZodError, z } from 'zod';
import { config } from 'dotenv'
import { expand } from 'dotenv-expand'
import { ZodError, z } from 'zod'
const stringBoolean = z.coerce
.string()
.transform((val) => {
return val === 'true';
return val === 'true'
})
.default('false');
.default('false')
const EnvSchema = z.object({
ADMIN_USERNAME: z.string(),
@ -20,6 +20,7 @@ const EnvSchema = z.object({
DB_MIGRATING: stringBoolean,
DB_SEEDING: stringBoolean,
NODE_ENV: z.string().default('development'),
ORIGIN: z.string(),
PUBLIC_SITE_NAME: z.string(),
PUBLIC_SITE_URL: z.string(),
PUBLIC_UMAMI_DO_NOT_TRACK: z.string(),
@ -27,26 +28,25 @@ const EnvSchema = z.object({
PUBLIC_UMAMI_URL: z.string(),
REDIS_URL: z.string(),
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 {
EnvSchema.parse(process.env);
EnvSchema.parse(process.env)
} catch (error) {
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) {
message += `${issue.path[0]}\n`;
message += `${issue.path[0]}\n`
}
const e = new Error(message);
e.stack = '';
throw e;
} else {
console.error(error);
const e = new Error(message)
e.stack = ''
throw e
}
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 { hc } from 'hono/client';
import { sequence } from '@sveltejs/kit/hooks';
import { redirect, type Handle } from '@sveltejs/kit';
import type { ApiRoutes } from '$lib/server/api';
import { parseApiResponse } from '$lib/utils/api';
import { StatusCodes } from '$lib/constants/status-codes';
// 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
// });
import { StatusCodes } from '$lib/constants/status-codes'
import type { ApiRoutes } from '$lib/server/api'
import { parseApiResponse } from '$lib/utils/api'
import { type Handle, redirect } from '@sveltejs/kit'
import { sequence } from '@sveltejs/kit/hooks'
import { hc } from 'hono/client'
const apiClient: Handle = async ({ event, resolve }) => {
/* ------------------------------ Register api ------------------------------ */
@ -21,76 +12,31 @@ const apiClient: Handle = async ({ event, resolve }) => {
fetch: event.fetch,
headers: {
'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 ----------------------------- */
async function getAuthedUser() {
const { data } = await api.user.$get().then(parseApiResponse)
return data?.user;
return data?.user
}
async function getAuthedUserOrThrow() {
const { data } = await api.user.$get().then(parseApiResponse);
if (!data || !data.user) throw redirect(StatusCodes.TEMPORARY_REDIRECT, '/');
return data?.user;
const { data } = await api.user.$get().then(parseApiResponse)
if (!data || !data.user) throw redirect(StatusCodes.TEMPORARY_REDIRECT, '/')
return data?.user
}
/* ------------------------------ Set contexts ------------------------------ */
event.locals.api = api;
event.locals.parseApiResponse = parseApiResponse;
event.locals.getAuthedUser = getAuthedUser;
event.locals.getAuthedUserOrThrow = getAuthedUserOrThrow;
event.locals.api = api
event.locals.parseApiResponse = parseApiResponse
event.locals.getAuthedUser = getAuthedUser
event.locals.getAuthedUserOrThrow = getAuthedUserOrThrow
/* ----------------------------- Return response ---------------------------- */
const response = await resolve(event);
return response;
};
const response = await resolve(event)
return response
}
// export const authentication: Handle = async function ({ event, resolve }) {
// 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();
export const handle: Handle = sequence(apiClient)

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

View file

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

View file

@ -1,15 +1,15 @@
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 { 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 { CredentialsType } from '$db/tables'
import type { HonoTypes } from '../types'
@injectable()
export class MfaController implements Controller {

View file

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

View file

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

View file

@ -1,30 +1,28 @@
import 'reflect-metadata';
import { Hono } from 'hono';
import { inject, injectable } from 'tsyringe';
import { requireAuth } from "../middleware/auth.middleware";
import type { HonoTypes } from '../types';
import type { Controller } from '../interfaces/controller.interface';
import {WishlistsService} from "$lib/server/api/services/wishlists.service";
import 'reflect-metadata'
import type { Controller } from '$lib/server/api/common/interfaces/controller.interface'
import { WishlistsService } from '$lib/server/api/services/wishlists.service'
import { Hono } from 'hono'
import { inject, injectable } from 'tsyringe'
import { requireAuth } from '../middleware/auth.middleware'
import type { HonoTypes } from '../types'
@injectable()
export class WishlistController implements Controller {
controller = new Hono<HonoTypes>();
controller = new Hono<HonoTypes>()
constructor(
@inject(WishlistsService) private readonly wishlistsService: WishlistsService
) { }
constructor(@inject(WishlistsService) private readonly wishlistsService: WishlistsService) {}
routes() {
return this.controller
.get('/', requireAuth, async (c) => {
const user = c.var.user;
const wishlists = await this.wishlistsService.findAllByUserId(user.id);
return c.json({ wishlists });
const user = c.var.user
const wishlists = await this.wishlistsService.findAllByUserId(user.id)
return c.json({ wishlists })
})
.get('/:cuid', requireAuth, async (c) => {
const cuid = c.req.param('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 type { z } from 'zod'
import { collections } from '$lib/server/api/infrastructure/database/tables'
export const InsertCollectionSchema = createInsertSchema(collections, {
name: (schema) => schema.name.trim()
.min(3, { message: 'Must be at least 3 characters' })
.max(64, { message: 'Must be less than 64 characters' }).optional(),
name: (schema) =>
schema.name.trim().min(3, { message: 'Must be at least 3 characters' }).max(64, { message: 'Must be less than 64 characters' }).optional(),
}).omit({
id: true,
cuid: true,

View file

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

View file

@ -1,15 +1,15 @@
import { Table, getTableName, sql } from 'drizzle-orm';
import env from '../../../../../env';
import { db, pool } from './index';
import * as schema from './tables';
import * as seeds from './seeds';
import { Table, getTableName, sql } from 'drizzle-orm'
import env from '../../../../env'
import { db, pool } from '../packages/drizzle'
import * as seeds from './seeds'
import * as schema from './tables'
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) {
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 [
@ -41,11 +41,11 @@ for (const table of [
schema.wishlists,
]) {
// await db.delete(table); // clear tables without truncating / resetting ids
await resetTable(db, table);
await resetTable(db, table)
}
await seeds.roles(db);
await seeds.users(db);
await seeds.roles(db)
await seeds.users(db)
await pool.end();
process.exit();
await pool.end()
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 { Argon2id } from 'oslo/password';
import { type db } from '$lib/server/api/infrastructure/database';
import * as schema from '$lib/server/api/infrastructure/database/tables';
import users from './data/users.json';
import { config } from '../../../common/config';
import * as schema from '$lib/server/api/databases/tables'
import { type db } from '$lib/server/api/packages/drizzle'
import { eq } from 'drizzle-orm'
import { Argon2id } from 'oslo/password'
import { config } from '../../configs/config'
import users from './data/users.json'
type JsonUser = {
id: string;
username: string;
email: string;
password: string;
id: string
username: string
email: string
password: string
roles: {
name: string;
primary: boolean;
}[];
};
name: string
primary: boolean
}[]
}
type JsonRole = {
name: string;
primary: boolean;
};
name: string
primary: boolean
}
export default async function seed(db: db) {
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 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'))
console.log('Admin Role: ', adminRole);
console.log('Admin Role: ', adminRole)
const adminUser = await db
.insert(schema.usersTable)
.values({
@ -36,27 +36,19 @@ export default async function seed(db: db) {
verified: true,
})
.returning()
.onConflictDoNothing();
.onConflictDoNothing()
console.log('Admin user created.', adminUser);
console.log('Admin user created.', adminUser)
await db
.insert(schema.credentialsTable)
.values({
await db.insert(schema.credentialsTable).values({
user_id: adminUser[0].id,
type: schema.CredentialsType.PASSWORD,
secret_data: await new Argon2id().hash(`${config.ADMIN_PASSWORD}`),
});
})
await db
.insert(schema.collections)
.values({ user_id: adminUser[0].id })
.onConflictDoNothing();
await db.insert(schema.collections).values({ user_id: adminUser[0].id }).onConflictDoNothing()
await db
.insert(schema.wishlists)
.values({ user_id: adminUser[0].id })
.onConflictDoNothing();
await db.insert(schema.wishlists).values({ user_id: adminUser[0].id }).onConflictDoNothing()
await db
.insert(schema.user_roles)
@ -65,9 +57,9 @@ export default async function seed(db: db) {
role_id: adminRole[0].id,
primary: true,
})
.onConflictDoNothing();
.onConflictDoNothing()
console.log('Admin user given admin role.');
console.log('Admin user given admin role.')
await db
.insert(schema.user_roles)
@ -76,9 +68,9 @@ export default async function seed(db: db) {
role_id: userRole[0].id,
primary: false,
})
.onConflictDoNothing();
.onConflictDoNothing()
console.log('Admin user given user role.');
console.log('Admin user given user role.')
await Promise.all(
users.map(async (user) => {
const [insertedUser] = await db
@ -86,27 +78,29 @@ export default async function seed(db: db) {
.values({
...user,
})
.returning();
.returning()
await db.insert(schema.credentialsTable).values({
user_id: insertedUser?.id,
type: schema.CredentialsType.PASSWORD,
secret_data: await new Argon2id().hash(user.password),
})
await db.insert(schema.collections).values({ user_id: insertedUser?.id });
await db.insert(schema.wishlists).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 Promise.all(
user.roles.map(async (role: JsonRole) => {
const foundRole = await db.query.roles.findFirst({
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({
user_id: insertedUser?.id,
role_id: foundRole?.id,
primary: role?.primary,
});
})
}),
);
)
}),
);
)
}

View file

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

View file

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

View file

@ -1,8 +1,8 @@
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import { usersTable } from './users.table';
import { timestamps } from '../utils';
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 { usersTable } from './users.table'
export const collections = pgTable('collections', {
id: uuid('id').primaryKey().defaultRandom(),
@ -14,14 +14,13 @@ export const collections = pgTable('collections', {
.references(() => usersTable.id, { onDelete: 'cascade' }),
name: text('name').notNull().default('My Collection'),
...timestamps,
});
})
export const collection_relations = relations(collections, ({ one }) => ({
user: one(usersTable, {
fields: [collections.user_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 { type InferSelectModel } from 'drizzle-orm';
import { timestamps } from '../utils';
import { usersTable } from "./users.table";
import { timestamps } from '$lib/server/api/common/utils/table.utils'
import { type InferSelectModel } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { usersTable } from './users.table'
export enum CredentialsType {
SECRET = 'secret',
PASSWORD = 'password',
TOTP = 'totp',
HOTP = 'hotp'
HOTP = 'hotp',
}
export const credentialsTable = pgTable('credentials', {
@ -17,7 +17,7 @@ export const credentialsTable = pgTable('credentials', {
.references(() => usersTable.id, { onDelete: 'cascade' }),
type: text('type').notNull().default(CredentialsType.PASSWORD),
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 { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import {games} from './games';
import { timestamps } from '../utils';
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 { games } from './games'
export const expansions = pgTable('expansions', {
id: uuid('id').primaryKey().defaultRandom(),
@ -16,9 +16,9 @@ export const expansions = pgTable('expansions', {
.notNull()
.references(() => games.id, { onDelete: 'restrict', onUpdate: 'cascade' }),
...timestamps,
});
})
export type Expansions = InferSelectModel<typeof expansions>;
export type Expansions = InferSelectModel<typeof expansions>
export const expansion_relations = relations(expansions, ({ one }) => ({
baseGame: one(games, {
@ -29,4 +29,4 @@ export const expansion_relations = relations(expansions, ({ one }) => ({
fields: [expansions.game_id],
references: [games.id],
}),
}));
}))

View file

@ -1,7 +1,7 @@
import { pgTable, text, uuid } from "drizzle-orm/pg-core";
import { type InferSelectModel } from 'drizzle-orm';
import { usersTable } from "./users.table";
import { timestamps } from '../utils';
import { timestamps } from '$lib/server/api/common/utils/table.utils'
import { type InferSelectModel } from 'drizzle-orm'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { usersTable } from './users.table'
export const federatedIdentityTable = pgTable('federated_identity', {
id: uuid('id').primaryKey().defaultRandom(),
@ -11,7 +11,7 @@ export const federatedIdentityTable = pgTable('federated_identity', {
identity_provider: text('identity_provider').notNull(),
federated_user_id: text('federated_user_id').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 { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations, sql } from 'drizzle-orm';
import {categories_to_games_table} from './categoriesToGames';
import {gamesToExternalIds} from './gamesToExternalIds';
import {mechanics_to_games} from './mechanicsToGames';
import {publishers_to_games} from './publishersToGames';
import { timestamps } from '../utils';
import { timestamps } from '$lib/server/api/common/utils/table.utils'
import { createId as cuid2 } from '@paralleldrive/cuid2'
import { type InferSelectModel, relations, sql } from 'drizzle-orm'
import { index, integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
import { categories_to_games_table } from './categoriesToGames'
import { gamesToExternalIds } from './gamesToExternalIds'
import { mechanics_to_games } from './mechanicsToGames'
import { publishers_to_games } from './publishersToGames'
export const games = pgTable(
'games',
@ -39,13 +39,13 @@ export const games = pgTable(
)`,
),
}),
);
)
export const gameRelations = relations(games, ({ many }) => ({
categories_to_games: many(categories_to_games_table),
mechanics_to_games: many(mechanics_to_games),
publishers_to_games: many(publishers_to_games),
gamesToExternalIds: many(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 { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import { usersTable } from './users.table';
import { timestamps } from '../utils';
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, timestamp, uuid } from 'drizzle-orm/pg-core'
import { usersTable } from './users.table'
export const password_reset_tokens = pgTable('password_reset_tokens', {
id: text('id')
@ -13,13 +13,13 @@ export const password_reset_tokens = pgTable('password_reset_tokens', {
.references(() => usersTable.id, { onDelete: 'cascade' }),
expires_at: timestamp('expires_at'),
...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 }) => ({
user: one(usersTable, {
fields: [password_reset_tokens.user_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 type { InferSelectModel } from 'drizzle-orm';
import { usersTable } from './users.table';
import { timestamps } from '../utils';
import { timestamps } from '$lib/server/api/common/utils/table.utils'
import type { InferSelectModel } from 'drizzle-orm'
import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { usersTable } from './users.table'
export const recoveryCodesTable = pgTable('recovery_codes', {
id: uuid('id').primaryKey().defaultRandom(),
@ -11,6 +11,6 @@ export const recoveryCodesTable = pgTable('recovery_codes', {
code: text('code').notNull(),
used: boolean('used').default(false),
...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 { type InferSelectModel, relations } from 'drizzle-orm';
import { boolean, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
import { timestamps } from '../utils';
import { usersTable } from './users.table';
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 { boolean, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
import { usersTable } from './users.table'
export const twoFactorTable = pgTable('two_factor', {
id: uuid('id').primaryKey().defaultRandom(),
@ -20,13 +20,13 @@ export const twoFactorTable = pgTable('two_factor', {
.references(() => usersTable.id)
.unique(),
...timestamps,
});
})
export const emailVerificationsRelations = relations(twoFactorTable, ({ one }) => ({
user: one(usersTable, {
fields: [twoFactorTable.userId],
references: [usersTable.id],
}),
}));
}))
export type TwoFactor = InferSelectModel<typeof twoFactorTable>;
export type TwoFactor = InferSelectModel<typeof twoFactorTable>

View file

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

View file

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

View file

@ -1,8 +1,8 @@
import { pgTable, text, uuid } from 'drizzle-orm/pg-core';
import { createId as cuid2 } from '@paralleldrive/cuid2';
import { type InferSelectModel, relations } from 'drizzle-orm';
import { usersTable } from './users.table';
import { timestamps } from '../utils';
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 { usersTable } from './users.table'
export const wishlists = pgTable('wishlists', {
id: uuid('id').primaryKey().defaultRandom(),
@ -14,13 +14,13 @@ export const wishlists = pgTable('wishlists', {
.references(() => usersTable.id, { onDelete: 'cascade' }),
name: text('name').notNull().default('My Wishlist'),
...timestamps,
});
})
export type Wishlists = InferSelectModel<typeof wishlists>;
export type Wishlists = InferSelectModel<typeof wishlists>
export const wishlists_relations = relations(wishlists, ({ one }) => ({
user: one(usersTable, {
fields: [wishlists.user_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 { Hono } from 'hono';
import { hc } from 'hono/client';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { validateAuthSession, verifyOrigin } from './middleware/auth.middleware';
import { config } from './common/config';
import { container } from 'tsyringe';
import { IamController } from './controllers/iam.controller';
import { LoginController } from './controllers/login.controller';
import { MfaController} from "$lib/server/api/controllers/mfa.controller";
import {UserController} from "$lib/server/api/controllers/user.controller";
import {SignupController} from "$lib/server/api/controllers/signup.controller";
import {WishlistController} from "$lib/server/api/controllers/wishlist.controller";
import {CollectionController} from "$lib/server/api/controllers/collection.controller";
import { CollectionController } from '$lib/server/api/controllers/collection.controller'
import { MfaController } from '$lib/server/api/controllers/mfa.controller'
import { SignupController } from '$lib/server/api/controllers/signup.controller'
import { UserController } from '$lib/server/api/controllers/user.controller'
import { WishlistController } from '$lib/server/api/controllers/wishlist.controller'
import { AuthCleanupJobs } from '$lib/server/api/jobs/auth-cleanup.job'
import { Hono } from 'hono'
import { hc } from 'hono/client'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { container } from 'tsyringe'
import { config } from './configs/config'
import { IamController } from './controllers/iam.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);
app.use(logger());
/* -------------------------------------------------------------------------- */
/* Global Middlewares */
/* -------------------------------------------------------------------------- */
app.use(verifyOrigin).use(validateAuthSession)
app.use(logger())
app.use(
'/*',
cors({
origin: [
'http://localhost:5173',
'http://localhost:80',
'http://host.docker.internal:80',
'http://host.docker.internal:5173'
], // Replace with your allowed domains
origin: ['http://localhost:5173', 'http://localhost:80', 'http://host.docker.internal:80', 'http://host.docker.internal:5173'], // Replace with your allowed domains
allowMethods: ['POST'],
allowHeaders: ['Content-Type']
allowHeaders: ['Content-Type'],
// credentials: true, // If you need to send cookies or HTTP authentication
})
);
}),
)
/* --------------------------------- Routes --------------------------------- */
/* -------------------------------------------------------------------------- */
/* Routes */
/* -------------------------------------------------------------------------- */
const routes = app
.route('/me', container.resolve(IamController).routes())
.route('/user', container.resolve(UserController).routes())
@ -46,14 +48,17 @@ const routes = app
.route('/wishlists', container.resolve(WishlistController).routes())
.route('/collections', container.resolve(CollectionController).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 */
/* -------------------------------------------------------------------------- */
export type AppType = typeof routes;
export const rpc = hc<AppType>(config.ORIGIN);
export type ApiClient = typeof rpc;
export type ApiRoutes = typeof routes;
export { app };
export const rpc = hc<typeof routes>(config.ORIGIN)
export type ApiClient = typeof rpc
export type ApiRoutes = typeof routes

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 { 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 { 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) => {
if (c.req.method === 'GET') {

View file

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

View file

@ -1,7 +1,7 @@
import { drizzle } from 'drizzle-orm/node-postgres';
import pg from 'pg';
import { config } from '../../common/config';
import * as schema from './tables';
import { drizzle } from 'drizzle-orm/node-postgres'
import pg from 'pg'
import { config } from '../configs/config'
import * as schema from '../databases/tables'
// create the connection
export const pool = new pg.Pool({
@ -12,11 +12,11 @@ export const pool = new pg.Pool({
database: config.DATABASE_DB,
ssl: config.DATABASE_HOST !== 'localhost',
max: config.DB_MIGRATING || config.DB_SEEDING ? 1 : undefined,
});
})
export const db = drizzle(pool, {
schema,
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
import { Lucia, TimeSpan } from 'lucia';
import { DrizzlePostgreSQLAdapter } from '@lucia-auth/adapter-drizzle';
import { db } from '../database';
import { sessionsTable, usersTable } from '../database/tables';
import { config } from '../../common/config';
import { Lucia, TimeSpan } from 'lucia'
import { config } from '../configs/config'
import { sessionsTable, usersTable } from '../databases/tables'
import { db } from './drizzle'
const adapter = new DrizzlePostgreSQLAdapter(db, sessionsTable, usersTable);
const adapter = new DrizzlePostgreSQLAdapter(db, sessionsTable, usersTable)
export const lucia = new Lucia(adapter, {
getSessionAttributes: (attributes) => {
@ -14,7 +14,7 @@ export const lucia = new Lucia(adapter, {
ipAddress: attributes.ip_address,
isTwoFactorAuthEnabled: attributes.twoFactorAuthEnabled,
isTwoFactorAuthenticated: attributes.isTwoFactorAuthenticated,
};
}
},
getUserAttributes: (attributes) => {
return {
@ -25,7 +25,7 @@ export const lucia = new Lucia(adapter, {
lastName: attributes.last_name,
mfa_enabled: attributes.mfa_enabled,
theme: attributes.theme,
};
}
},
sessionExpiresIn: new TimeSpan(2, 'w'), // 2 weeks
sessionCookie: {
@ -38,26 +38,26 @@ export const lucia = new Lucia(adapter, {
domain: config.domain,
},
},
});
})
declare module 'lucia' {
interface Register {
Lucia: typeof lucia;
DatabaseUserAttributes: DatabaseUserAttributes;
DatabaseSessionAttributes: DatabaseSessionAttributes;
Lucia: typeof lucia
DatabaseUserAttributes: DatabaseUserAttributes
DatabaseSessionAttributes: DatabaseSessionAttributes
}
interface DatabaseSessionAttributes {
ip_country: string;
ip_address: string;
twoFactorAuthEnabled: boolean;
isTwoFactorAuthenticated: boolean;
ip_country: string
ip_address: string
twoFactorAuthEnabled: boolean
isTwoFactorAuthenticated: boolean
}
interface DatabaseUserAttributes {
username: string;
email: string;
first_name: string;
last_name: string;
mfa_enabled: boolean;
theme: string;
username: string
email: string
first_name: string
last_name: string
mfa_enabled: boolean
theme: string
}
}

View file

@ -1,11 +1,11 @@
import { container } from 'tsyringe';
import { db } from '../infrastructure/database';
import { container } from 'tsyringe'
import { db } from '../packages/drizzle'
// Symbol
export const DatabaseProvider = Symbol('DATABASE_TOKEN');
export const DatabaseProvider = Symbol('DATABASE_TOKEN')
// Type
export type DatabaseProvider = typeof db;
export type DatabaseProvider = typeof db
// 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 { lucia } from '../infrastructure/auth/lucia';
import { container } from 'tsyringe'
import { lucia } from '../packages/lucia'
// Symbol
export const LuciaProvider = Symbol('LUCIA_PROVIDER');
export const LuciaProvider = Symbol('LUCIA_PROVIDER')
// Type
export type LuciaProvider = typeof lucia;
export type LuciaProvider = typeof lucia
// 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 { config } from '../common/config';
import { container } from 'tsyringe'
import { config } from '../configs/config'
// Symbol
export const RedisProvider = Symbol('REDIS_TOKEN');
// Type
export type RedisProvider = RedisClient;
// Register
export const RedisProvider = Symbol('REDIS_TOKEN')
export type RedisProvider = RedisClient
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 { eq, type InferInsertModel } from "drizzle-orm";
import {DatabaseProvider} from "$lib/server/api/providers";
import { collections } from "../infrastructure/database/tables";
import { takeFirstOrThrow } from "../infrastructure/database/utils";
import type { Repository } from '$lib/server/api/common/interfaces/repository.interface'
import { takeFirstOrThrow } from '$lib/server/api/common/utils/repository.utils'
import { DatabaseProvider } from '$lib/server/api/providers/database.provider'
import { type InferInsertModel, eq } from 'drizzle-orm'
import { inject, injectable } from 'tsyringe'
import { collections } from '../databases/tables'
export type CreateCollection = InferInsertModel<typeof collections>;
export type UpdateCollection = Partial<CreateCollection>;
export type CreateCollection = InferInsertModel<typeof collections>
export type UpdateCollection = Partial<CreateCollection>
@injectable()
export class CollectionsRepository {
export class CollectionsRepository implements Repository {
constructor(@inject(DatabaseProvider) private readonly db: DatabaseProvider) {}
async findAll() {
return this.db.query.collections.findMany();
return this.db.query.collections.findMany()
}
async findOneById(id: string) {
@ -20,8 +21,8 @@ export class CollectionsRepository {
where: eq(collections.id, id),
columns: {
cuid: true,
name: true
}
name: true,
},
})
}
@ -30,8 +31,8 @@ export class CollectionsRepository {
where: eq(collections.cuid, cuid),
columns: {
cuid: true,
name: true
}
name: true,
},
})
}
@ -40,27 +41,26 @@ export class CollectionsRepository {
where: eq(collections.user_id, userId),
columns: {
cuid: true,
name: true
}
name: true,
},
})
}
async findAllByUserId(userId: string) {
return this.db.query.collections.findMany({
where: eq(collections.user_id, userId)
where: eq(collections.user_id, userId),
})
}
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) {
return this.db
.update(collections)
.set(data)
.where(eq(collections.id, id))
.returning()
.then(takeFirstOrThrow);
return this.db.update(collections).set(data).where(eq(collections.id, id)).returning().then(takeFirstOrThrow)
}
trxHost(trx: DatabaseProvider) {
return new CollectionsRepository(trx)
}
}

View file

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