From 3aa537f389a2598e608d04267dd7d7e9d458b012 Mon Sep 17 00:00:00 2001 From: Bradley Shellnut Date: Sun, 1 Sep 2024 12:22:00 -0700 Subject: [PATCH] Refactoring to match simplifying done on the origin TaroStack. --- .gitignore | 2 + drizzle.config.ts | 16 +- oldApis/collection/[id]/search/+server.ts | 51 +- oldApis/reset-password/+server.ts | 28 +- oldApis/reset-password/[token]/+server.ts | 24 +- package.json | 4 +- src/env.ts | 34 +- src/hooks.server.ts | 96 +--- .../api/common/{errors.ts => exceptions.ts} | 0 .../common/interfaces/controller.interface.ts | 8 + .../api/common/interfaces/email.interface.ts | 4 + .../common/interfaces/repository.interface.ts | 5 + .../api/common/utils/repository.utils.ts | 14 + .../server/api/common/utils/table.utils.ts | 29 ++ .../server/api/{common => configs}/config.ts | 0 .../api/controllers/collection.controller.ts | 34 +- .../server/api/controllers/iam.controller.ts | 26 +- .../api/controllers/login.controller.ts | 65 +-- .../server/api/controllers/mfa.controller.ts | 18 +- .../api/controllers/signup.controller.ts | 91 ++-- .../server/api/controllers/user.controller.ts | 38 +- .../api/controllers/wishlist.controller.ts | 30 +- src/lib/server/api/databases/migrate.ts | 26 + .../migrations/0000_dazzling_stick.sql | 0 .../migrations/0001_noisy_sally_floyd.sql | 0 .../migrations/0002_fancy_valkyrie.sql | 0 .../migrations/0003_worried_taskmaster.sql | 0 .../migrations/0004_heavy_sphinx.sql | 0 .../migrations/0005_true_mathemanic.sql | 0 .../migrations/meta/0000_snapshot.json | 0 .../migrations/meta/0001_snapshot.json | 0 .../migrations/meta/0002_snapshot.json | 0 .../migrations/meta/0003_snapshot.json | 0 .../migrations/meta/0004_snapshot.json | 0 .../migrations/meta/0005_snapshot.json | 0 .../migrations/meta/_journal.json | 0 .../schemas/collections.schema.ts | 7 +- .../schemas/users.schemas.ts | 2 +- .../database => databases}/seed.ts | 24 +- .../seeds/data/roles.json | 0 .../seeds/data/users.json | 0 .../database => databases}/seeds/index.ts | 0 src/lib/server/api/databases/seeds/roles.ts | 11 + .../database => databases}/seeds/users.ts | 92 ++-- .../tables/categories.table.ts | 18 +- .../tables/categoriesToExternalIdsTable.ts | 0 .../tables/categoriesToGames.ts | 0 .../tables/collectionItems.ts | 18 +- .../tables/collections.ts | 17 +- .../tables/credentials.table.ts | 16 +- .../tables/expansions.ts | 16 +- .../tables/externalIds.ts | 0 .../tables/federatedIdentity.table.ts | 14 +- .../database => databases}/tables/games.ts | 22 +- .../tables/gamesToExternalIds.ts | 0 .../database => databases}/tables/index.ts | 0 .../server/api/databases/tables/mechanics.ts | 23 + .../tables/mechanicsToExternalIds.ts | 0 .../tables/mechanicsToGames.ts | 0 .../tables/passwordResetTokens.ts | 16 +- .../server/api/databases/tables/publishers.ts | 23 + .../tables/publishersToExternalIds.ts | 0 .../tables/publishersToGames.ts | 0 .../tables/recovery-codes.table.ts | 12 +- src/lib/server/api/databases/tables/roles.ts | 21 + .../tables/sessions.table.ts | 0 .../tables/two-factor.table.ts | 16 +- .../tables/userRoles.ts | 18 +- .../tables/users.table.ts | 6 +- .../tables/wishlistItems.ts | 18 +- .../tables/wishlists.ts | 16 +- .../server/api/dtos/create-user-role.dto.ts | 1 + src/lib/server/api/dtos/id-params.dto.ts | 5 + .../api/dtos/register-emailpassword.dto.ts | 20 + .../server/api/dtos/signin-username.dto.ts | 12 + .../api/dtos/signup-username-email.dto.ts | 24 + src/lib/server/api/dtos/update-email.dto.ts | 11 + src/lib/server/api/dtos/update-profile.dto.ts | 23 + .../server/api/dtos/verify-password.dto.ts | 7 + src/lib/server/api/dtos/verify-totp.dto.ts | 11 + .../email-change-notice.hbs | 0 src/lib/server/api/index.ts | 91 ++-- .../api/infrastructure/database/migrate.ts | 26 - .../infrastructure/database/seeds/roles.ts | 11 - .../database/tables/mechanics.ts | 23 - .../database/tables/publishers.ts | 23 - .../infrastructure/database/tables/roles.ts | 21 - .../api/infrastructure/database/utils.ts | 42 -- .../api/interfaces/controller.interface.ts | 8 - src/lib/server/api/jobs/auth-cleanup.job.ts | 42 ++ .../server/api/middleware/auth.middleware.ts | 8 +- .../api/middleware/rate-limiter.middleware.ts | 54 +- .../database/index.ts => packages/drizzle.ts} | 14 +- .../auth => packages}/lucia.ts | 44 +- .../server/api/providers/database.provider.ts | 10 +- src/lib/server/api/providers/index.ts | 3 - .../server/api/providers/lucia.provider.ts | 10 +- .../server/api/providers/redis.provider.ts | 19 +- .../repositories/collections.repository.ts | 50 +- .../repositories/credentials.repository.ts | 15 +- .../api/repositories/roles.repository.ts | 60 ++- .../api/repositories/user_roles.repository.ts | 45 +- .../api/repositories/users.repository.ts | 56 +-- .../api/repositories/wishlists.repository.ts | 52 +- src/lib/server/api/services/iam.service.ts | 49 +- src/lib/server/api/services/jobs.service.ts | 16 + .../api/services/loginrequest.service.ts | 147 +++--- src/lib/server/api/services/mailer.service.ts | 126 ++--- src/lib/server/api/services/queues.service.ts | 19 - src/lib/server/api/services/totp.service.ts | 44 +- src/lib/server/api/services/users.service.ts | 66 +-- src/lib/server/auth-utils.ts | 24 +- src/lib/utils/db/categoryUtils.ts | 64 ++- src/lib/utils/db/expansionUtils.ts | 39 +- src/lib/utils/db/gameUtils.ts | 134 +++-- src/lib/utils/db/mechanicUtils.ts | 46 +- src/lib/utils/db/publisherUtils.ts | 68 ++- .../(app)/(protected)/admin/+layout.server.ts | 36 +- .../(protected)/admin/users/+page.server.ts | 31 +- .../admin/users/[id]/+page.server.ts | 112 ++--- .../(protected)/collections/+page.server.ts | 107 ++-- .../collections/[cuid]/+page.server.ts | 131 +++-- .../(app)/(protected)/list/+layout.server.ts | 24 +- .../(protected)/list/[id]/+page.server.ts | 85 ++-- .../(app)/(protected)/profile/+page.server.ts | 219 ++++---- .../(app)/(protected)/profile/+page.svelte | 75 +-- .../profile/security/mfa/+page.server.ts | 34 +- .../mfa/recovery-codes/+page.server.ts | 24 +- .../security/password/change/+page.server.ts | 95 ++-- .../(protected)/wishlists/+page.server.ts | 131 +++-- .../wishlists/[cuid]/+page.server.ts | 128 +++-- src/routes/(app)/+page.server.ts | 44 +- src/routes/(app)/game/[id]/+page.server.ts | 128 ++--- src/routes/(auth)/login/+page.server.ts | 12 +- src/routes/(auth)/signup/+page.server.ts | 50 +- src/routes/(auth)/signup/+page.svelte | 62 +-- src/routes/(auth)/totp/+page.server.ts | 469 ++++++++---------- 137 files changed, 2344 insertions(+), 2405 deletions(-) rename src/lib/server/api/common/{errors.ts => exceptions.ts} (100%) create mode 100644 src/lib/server/api/common/interfaces/controller.interface.ts create mode 100644 src/lib/server/api/common/interfaces/email.interface.ts create mode 100644 src/lib/server/api/common/interfaces/repository.interface.ts create mode 100644 src/lib/server/api/common/utils/repository.utils.ts create mode 100644 src/lib/server/api/common/utils/table.utils.ts rename src/lib/server/api/{common => configs}/config.ts (100%) create mode 100644 src/lib/server/api/databases/migrate.ts rename src/lib/server/api/{infrastructure/database => databases}/migrations/0000_dazzling_stick.sql (100%) rename src/lib/server/api/{infrastructure/database => databases}/migrations/0001_noisy_sally_floyd.sql (100%) rename src/lib/server/api/{infrastructure/database => databases}/migrations/0002_fancy_valkyrie.sql (100%) rename src/lib/server/api/{infrastructure/database => databases}/migrations/0003_worried_taskmaster.sql (100%) rename src/lib/server/api/{infrastructure/database => databases}/migrations/0004_heavy_sphinx.sql (100%) rename src/lib/server/api/{infrastructure/database => databases}/migrations/0005_true_mathemanic.sql (100%) rename src/lib/server/api/{infrastructure/database => databases}/migrations/meta/0000_snapshot.json (100%) rename src/lib/server/api/{infrastructure/database => databases}/migrations/meta/0001_snapshot.json (100%) rename src/lib/server/api/{infrastructure/database => databases}/migrations/meta/0002_snapshot.json (100%) rename src/lib/server/api/{infrastructure/database => databases}/migrations/meta/0003_snapshot.json (100%) rename src/lib/server/api/{infrastructure/database => databases}/migrations/meta/0004_snapshot.json (100%) rename src/lib/server/api/{infrastructure/database => databases}/migrations/meta/0005_snapshot.json (100%) rename src/lib/server/api/{infrastructure/database => databases}/migrations/meta/_journal.json (100%) rename src/lib/server/api/{infrastructure/database => databases}/schemas/collections.schema.ts (68%) rename src/lib/server/api/{infrastructure/database => databases}/schemas/users.schemas.ts (92%) rename src/lib/server/api/{infrastructure/database => databases}/seed.ts (73%) rename src/lib/server/api/{infrastructure/database => databases}/seeds/data/roles.json (100%) rename src/lib/server/api/{infrastructure/database => databases}/seeds/data/users.json (100%) rename src/lib/server/api/{infrastructure/database => databases}/seeds/index.ts (100%) create mode 100644 src/lib/server/api/databases/seeds/roles.ts rename src/lib/server/api/{infrastructure/database => databases}/seeds/users.ts (53%) rename src/lib/server/api/{infrastructure/database => databases}/tables/categories.table.ts (54%) rename src/lib/server/api/{infrastructure/database => databases}/tables/categoriesToExternalIdsTable.ts (100%) rename src/lib/server/api/{infrastructure/database => databases}/tables/categoriesToGames.ts (100%) rename src/lib/server/api/{infrastructure/database => databases}/tables/collectionItems.ts (75%) rename src/lib/server/api/{infrastructure/database => databases}/tables/collections.ts (58%) rename src/lib/server/api/{infrastructure/database => databases}/tables/credentials.table.ts (64%) rename src/lib/server/api/{infrastructure/database => databases}/tables/expansions.ts (65%) rename src/lib/server/api/{infrastructure/database => databases}/tables/externalIds.ts (100%) rename src/lib/server/api/{infrastructure/database => databases}/tables/federatedIdentity.table.ts (63%) rename src/lib/server/api/{infrastructure/database => databases}/tables/games.ts (74%) rename src/lib/server/api/{infrastructure/database => databases}/tables/gamesToExternalIds.ts (100%) rename src/lib/server/api/{infrastructure/database => databases}/tables/index.ts (100%) create mode 100644 src/lib/server/api/databases/tables/mechanics.ts rename src/lib/server/api/{infrastructure/database => databases}/tables/mechanicsToExternalIds.ts (100%) rename src/lib/server/api/{infrastructure/database => databases}/tables/mechanicsToGames.ts (100%) rename src/lib/server/api/{infrastructure/database => databases}/tables/passwordResetTokens.ts (70%) create mode 100644 src/lib/server/api/databases/tables/publishers.ts rename src/lib/server/api/{infrastructure/database => databases}/tables/publishersToExternalIds.ts (100%) rename src/lib/server/api/{infrastructure/database => databases}/tables/publishersToGames.ts (100%) rename src/lib/server/api/{infrastructure/database => databases}/tables/recovery-codes.table.ts (68%) create mode 100644 src/lib/server/api/databases/tables/roles.ts rename src/lib/server/api/{infrastructure/database => databases}/tables/sessions.table.ts (100%) rename src/lib/server/api/{infrastructure/database => databases}/tables/two-factor.table.ts (68%) rename src/lib/server/api/{infrastructure/database => databases}/tables/userRoles.ts (68%) rename src/lib/server/api/{infrastructure/database => databases}/tables/users.table.ts (92%) rename src/lib/server/api/{infrastructure/database => databases}/tables/wishlistItems.ts (68%) rename src/lib/server/api/{infrastructure/database => databases}/tables/wishlists.ts (58%) create mode 100644 src/lib/server/api/dtos/create-user-role.dto.ts create mode 100644 src/lib/server/api/dtos/id-params.dto.ts create mode 100644 src/lib/server/api/dtos/register-emailpassword.dto.ts create mode 100644 src/lib/server/api/dtos/signin-username.dto.ts create mode 100644 src/lib/server/api/dtos/signup-username-email.dto.ts create mode 100644 src/lib/server/api/dtos/update-email.dto.ts create mode 100644 src/lib/server/api/dtos/update-profile.dto.ts create mode 100644 src/lib/server/api/dtos/verify-password.dto.ts create mode 100644 src/lib/server/api/dtos/verify-totp.dto.ts rename src/lib/server/api/{infrastructure/email-templates => emails}/email-change-notice.hbs (100%) delete mode 100644 src/lib/server/api/infrastructure/database/migrate.ts delete mode 100644 src/lib/server/api/infrastructure/database/seeds/roles.ts delete mode 100644 src/lib/server/api/infrastructure/database/tables/mechanics.ts delete mode 100644 src/lib/server/api/infrastructure/database/tables/publishers.ts delete mode 100644 src/lib/server/api/infrastructure/database/tables/roles.ts delete mode 100644 src/lib/server/api/infrastructure/database/utils.ts delete mode 100644 src/lib/server/api/interfaces/controller.interface.ts create mode 100644 src/lib/server/api/jobs/auth-cleanup.job.ts rename src/lib/server/api/{infrastructure/database/index.ts => packages/drizzle.ts} (68%) rename src/lib/server/api/{infrastructure/auth => packages}/lucia.ts (66%) delete mode 100644 src/lib/server/api/providers/index.ts create mode 100644 src/lib/server/api/services/jobs.service.ts delete mode 100644 src/lib/server/api/services/queues.service.ts diff --git a/.gitignore b/.gitignore index 88ad69d..15629aa 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ node_modules .env.* *.xdp* !.env.example +vite.config.js.timestamp-* +vite.config.ts.timestamp-* .vercel .output .idea diff --git a/drizzle.config.ts b/drizzle.config.ts index e981991..f4618ee 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -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', + }, +}) diff --git a/oldApis/collection/[id]/search/+server.ts b/oldApis/collection/[id]/search/+server.ts index 528c6f6..37e442e 100644 --- a/oldApis/collection/[id]/search/+server.ts +++ b/oldApis/collection/[id]/search/+server.ts @@ -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' }) } } diff --git a/oldApis/reset-password/+server.ts b/oldApis/reset-password/+server.ts index c661f15..3187ce2 100644 --- a/oldApis/reset-password/+server.ts +++ b/oldApis/reset-password/+server.ts @@ -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, - }); + }) } diff --git a/oldApis/reset-password/[token]/+server.ts b/oldApis/reset-password/[token]/+server.ts index 2b50676..8d6360b 100644 --- a/oldApis/reset-password/[token]/+server.ts +++ b/oldApis/reset-password/[token]/+server.ts @@ -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(), }, - }); + }) } diff --git a/package.json b/package.json index dd9738d..7fea7c8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/env.ts b/src/env.ts index 33366fa..fd60759 100644 --- a/src/env.ts +++ b/src/env.ts @@ -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; +export type EnvSchema = z.infer -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) diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 6b91f8b..6b9521d 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -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) diff --git a/src/lib/server/api/common/errors.ts b/src/lib/server/api/common/exceptions.ts similarity index 100% rename from src/lib/server/api/common/errors.ts rename to src/lib/server/api/common/exceptions.ts diff --git a/src/lib/server/api/common/interfaces/controller.interface.ts b/src/lib/server/api/common/interfaces/controller.interface.ts new file mode 100644 index 0000000..7bb91a2 --- /dev/null +++ b/src/lib/server/api/common/interfaces/controller.interface.ts @@ -0,0 +1,8 @@ +import { Hono } from 'hono' +import type { BlankSchema } from 'hono/types' +import type { HonoTypes } from '../../types' + +export interface Controller { + controller: Hono + routes(): any +} diff --git a/src/lib/server/api/common/interfaces/email.interface.ts b/src/lib/server/api/common/interfaces/email.interface.ts new file mode 100644 index 0000000..ae19964 --- /dev/null +++ b/src/lib/server/api/common/interfaces/email.interface.ts @@ -0,0 +1,4 @@ +export interface Email { + subject(): string + html(): string +} diff --git a/src/lib/server/api/common/interfaces/repository.interface.ts b/src/lib/server/api/common/interfaces/repository.interface.ts new file mode 100644 index 0000000..fc96e24 --- /dev/null +++ b/src/lib/server/api/common/interfaces/repository.interface.ts @@ -0,0 +1,5 @@ +import type { DatabaseProvider } from '$lib/server/api/providers/database.provider' + +export interface Repository { + trxHost(trx: DatabaseProvider): any +} diff --git a/src/lib/server/api/common/utils/repository.utils.ts b/src/lib/server/api/common/utils/repository.utils.ts new file mode 100644 index 0000000..1310269 --- /dev/null +++ b/src/lib/server/api/common/utils/repository.utils.ts @@ -0,0 +1,14 @@ +import { HTTPException } from 'hono/http-exception' + +export const takeFirst = (values: T[]): T | null => { + if (values.length === 0) return null + return values[0] as T +} + +export const takeFirstOrThrow = (values: T[]): T => { + if (values.length === 0) + throw new HTTPException(404, { + message: 'Resource not found', + }) + return values[0] as T +} diff --git a/src/lib/server/api/common/utils/table.utils.ts b/src/lib/server/api/common/utils/table.utils.ts new file mode 100644 index 0000000..a9fe09d --- /dev/null +++ b/src/lib/server/api/common/utils/table.utils.ts @@ -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(), +} diff --git a/src/lib/server/api/common/config.ts b/src/lib/server/api/configs/config.ts similarity index 100% rename from src/lib/server/api/common/config.ts rename to src/lib/server/api/configs/config.ts diff --git a/src/lib/server/api/controllers/collection.controller.ts b/src/lib/server/api/controllers/collection.controller.ts index 8f61bd1..169e6b5 100644 --- a/src/lib/server/api/controllers/collection.controller.ts +++ b/src/lib/server/api/controllers/collection.controller.ts @@ -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(); + controller = new Hono() - 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 }) + }) } } diff --git a/src/lib/server/api/controllers/iam.controller.ts b/src/lib/server/api/controllers/iam.controller.ts index 0f34305..df28fb0 100644 --- a/src/lib/server/api/controllers/iam.controller.ts +++ b/src/lib/server/api/controllers/iam.controller.ts @@ -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 { diff --git a/src/lib/server/api/controllers/login.controller.ts b/src/lib/server/api/controllers/login.controller.ts index c86cefc..df9ed50 100644 --- a/src/lib/server/api/controllers/login.controller.ts +++ b/src/lib/server/api/controllers/login.controller.ts @@ -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(); + controller = new Hono() 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); - setCookie(c, sessionCookie.name, sessionCookie.value, { - path: sessionCookie.attributes.path, - maxAge: sessionCookie?.attributes?.maxAge && sessionCookie?.attributes?.maxAge < new TimeSpan(365, 'd').seconds() - ? sessionCookie.attributes.maxAge : new TimeSpan(2, 'w').seconds(), - domain: sessionCookie.attributes.domain, - sameSite: sessionCookie.attributes.sameSite as any, - secure: sessionCookie.attributes.secure, - httpOnly: sessionCookie.attributes.httpOnly, - expires: sessionCookie.attributes.expires - }); - return c.json({ message: 'ok' }); + return this.controller.post('/', 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(), + 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' }) + }) } } diff --git a/src/lib/server/api/controllers/mfa.controller.ts b/src/lib/server/api/controllers/mfa.controller.ts index dbbc13c..148b390 100644 --- a/src/lib/server/api/controllers/mfa.controller.ts +++ b/src/lib/server/api/controllers/mfa.controller.ts @@ -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 { diff --git a/src/lib/server/api/controllers/signup.controller.ts b/src/lib/server/api/controllers/signup.controller.ts index f8070e9..67a7b24 100644 --- a/src/lib/server/api/controllers/signup.controller.ts +++ b/src/lib/server/api/controllers/signup.controller.ts @@ -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(); + controller = new Hono() constructor( - @inject(UsersService) private readonly usersService: UsersService, - @inject(LoginRequestsService) private readonly loginRequestService: LoginRequestsService, - @inject(LuciaProvider) private lucia: LuciaProvider - ) { } + @inject(UsersService) private readonly usersService: UsersService, + @inject(LoginRequestsService) private readonly loginRequestService: LoginRequestsService, + @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); - } + if (existingUser) { + 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); - } + if (!user) { + 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); - setCookie(c, sessionCookie.name, sessionCookie.value, { - path: sessionCookie.attributes.path, - maxAge: sessionCookie?.attributes?.maxAge && sessionCookie?.attributes?.maxAge < new TimeSpan(365, 'd').seconds() - ? sessionCookie.attributes.maxAge : new TimeSpan(2, 'w').seconds(), - domain: sessionCookie.attributes.domain, - sameSite: sessionCookie.attributes.sameSite as any, - secure: sessionCookie.attributes.secure, - httpOnly: sessionCookie.attributes.httpOnly, - expires: sessionCookie.attributes.expires - }); - return c.json({ message: 'ok' }); - }); + 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(), + 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' }) + }) } -} \ No newline at end of file +} diff --git a/src/lib/server/api/controllers/user.controller.ts b/src/lib/server/api/controllers/user.controller.ts index 90a0cc8..ec8d7a1 100644 --- a/src/lib/server/api/controllers/user.controller.ts +++ b/src/lib/server/api/controllers/user.controller.ts @@ -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(); + controller = new Hono() - 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 }) + }) } } diff --git a/src/lib/server/api/controllers/wishlist.controller.ts b/src/lib/server/api/controllers/wishlist.controller.ts index 90fdc15..9c19d37 100644 --- a/src/lib/server/api/controllers/wishlist.controller.ts +++ b/src/lib/server/api/controllers/wishlist.controller.ts @@ -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(); + controller = new Hono() - 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 }) + }) } } diff --git a/src/lib/server/api/databases/migrate.ts b/src/lib/server/api/databases/migrate.ts new file mode 100644 index 0000000..d476b12 --- /dev/null +++ b/src/lib/server/api/databases/migrate.ts @@ -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() diff --git a/src/lib/server/api/infrastructure/database/migrations/0000_dazzling_stick.sql b/src/lib/server/api/databases/migrations/0000_dazzling_stick.sql similarity index 100% rename from src/lib/server/api/infrastructure/database/migrations/0000_dazzling_stick.sql rename to src/lib/server/api/databases/migrations/0000_dazzling_stick.sql diff --git a/src/lib/server/api/infrastructure/database/migrations/0001_noisy_sally_floyd.sql b/src/lib/server/api/databases/migrations/0001_noisy_sally_floyd.sql similarity index 100% rename from src/lib/server/api/infrastructure/database/migrations/0001_noisy_sally_floyd.sql rename to src/lib/server/api/databases/migrations/0001_noisy_sally_floyd.sql diff --git a/src/lib/server/api/infrastructure/database/migrations/0002_fancy_valkyrie.sql b/src/lib/server/api/databases/migrations/0002_fancy_valkyrie.sql similarity index 100% rename from src/lib/server/api/infrastructure/database/migrations/0002_fancy_valkyrie.sql rename to src/lib/server/api/databases/migrations/0002_fancy_valkyrie.sql diff --git a/src/lib/server/api/infrastructure/database/migrations/0003_worried_taskmaster.sql b/src/lib/server/api/databases/migrations/0003_worried_taskmaster.sql similarity index 100% rename from src/lib/server/api/infrastructure/database/migrations/0003_worried_taskmaster.sql rename to src/lib/server/api/databases/migrations/0003_worried_taskmaster.sql diff --git a/src/lib/server/api/infrastructure/database/migrations/0004_heavy_sphinx.sql b/src/lib/server/api/databases/migrations/0004_heavy_sphinx.sql similarity index 100% rename from src/lib/server/api/infrastructure/database/migrations/0004_heavy_sphinx.sql rename to src/lib/server/api/databases/migrations/0004_heavy_sphinx.sql diff --git a/src/lib/server/api/infrastructure/database/migrations/0005_true_mathemanic.sql b/src/lib/server/api/databases/migrations/0005_true_mathemanic.sql similarity index 100% rename from src/lib/server/api/infrastructure/database/migrations/0005_true_mathemanic.sql rename to src/lib/server/api/databases/migrations/0005_true_mathemanic.sql diff --git a/src/lib/server/api/infrastructure/database/migrations/meta/0000_snapshot.json b/src/lib/server/api/databases/migrations/meta/0000_snapshot.json similarity index 100% rename from src/lib/server/api/infrastructure/database/migrations/meta/0000_snapshot.json rename to src/lib/server/api/databases/migrations/meta/0000_snapshot.json diff --git a/src/lib/server/api/infrastructure/database/migrations/meta/0001_snapshot.json b/src/lib/server/api/databases/migrations/meta/0001_snapshot.json similarity index 100% rename from src/lib/server/api/infrastructure/database/migrations/meta/0001_snapshot.json rename to src/lib/server/api/databases/migrations/meta/0001_snapshot.json diff --git a/src/lib/server/api/infrastructure/database/migrations/meta/0002_snapshot.json b/src/lib/server/api/databases/migrations/meta/0002_snapshot.json similarity index 100% rename from src/lib/server/api/infrastructure/database/migrations/meta/0002_snapshot.json rename to src/lib/server/api/databases/migrations/meta/0002_snapshot.json diff --git a/src/lib/server/api/infrastructure/database/migrations/meta/0003_snapshot.json b/src/lib/server/api/databases/migrations/meta/0003_snapshot.json similarity index 100% rename from src/lib/server/api/infrastructure/database/migrations/meta/0003_snapshot.json rename to src/lib/server/api/databases/migrations/meta/0003_snapshot.json diff --git a/src/lib/server/api/infrastructure/database/migrations/meta/0004_snapshot.json b/src/lib/server/api/databases/migrations/meta/0004_snapshot.json similarity index 100% rename from src/lib/server/api/infrastructure/database/migrations/meta/0004_snapshot.json rename to src/lib/server/api/databases/migrations/meta/0004_snapshot.json diff --git a/src/lib/server/api/infrastructure/database/migrations/meta/0005_snapshot.json b/src/lib/server/api/databases/migrations/meta/0005_snapshot.json similarity index 100% rename from src/lib/server/api/infrastructure/database/migrations/meta/0005_snapshot.json rename to src/lib/server/api/databases/migrations/meta/0005_snapshot.json diff --git a/src/lib/server/api/infrastructure/database/migrations/meta/_journal.json b/src/lib/server/api/databases/migrations/meta/_journal.json similarity index 100% rename from src/lib/server/api/infrastructure/database/migrations/meta/_journal.json rename to src/lib/server/api/databases/migrations/meta/_journal.json diff --git a/src/lib/server/api/infrastructure/database/schemas/collections.schema.ts b/src/lib/server/api/databases/schemas/collections.schema.ts similarity index 68% rename from src/lib/server/api/infrastructure/database/schemas/collections.schema.ts rename to src/lib/server/api/databases/schemas/collections.schema.ts index 84d7ecc..ecb84c8 100644 --- a/src/lib/server/api/infrastructure/database/schemas/collections.schema.ts +++ b/src/lib/server/api/databases/schemas/collections.schema.ts @@ -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, diff --git a/src/lib/server/api/infrastructure/database/schemas/users.schemas.ts b/src/lib/server/api/databases/schemas/users.schemas.ts similarity index 92% rename from src/lib/server/api/infrastructure/database/schemas/users.schemas.ts rename to src/lib/server/api/databases/schemas/users.schemas.ts index 96564a5..7d01d3e 100644 --- a/src/lib/server/api/infrastructure/database/schemas/users.schemas.ts +++ b/src/lib/server/api/databases/schemas/users.schemas.ts @@ -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(), diff --git a/src/lib/server/api/infrastructure/database/seed.ts b/src/lib/server/api/databases/seed.ts similarity index 73% rename from src/lib/server/api/infrastructure/database/seed.ts rename to src/lib/server/api/databases/seed.ts index 1223370..de83be1 100644 --- a/src/lib/server/api/infrastructure/database/seed.ts +++ b/src/lib/server/api/databases/seed.ts @@ -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() diff --git a/src/lib/server/api/infrastructure/database/seeds/data/roles.json b/src/lib/server/api/databases/seeds/data/roles.json similarity index 100% rename from src/lib/server/api/infrastructure/database/seeds/data/roles.json rename to src/lib/server/api/databases/seeds/data/roles.json diff --git a/src/lib/server/api/infrastructure/database/seeds/data/users.json b/src/lib/server/api/databases/seeds/data/users.json similarity index 100% rename from src/lib/server/api/infrastructure/database/seeds/data/users.json rename to src/lib/server/api/databases/seeds/data/users.json diff --git a/src/lib/server/api/infrastructure/database/seeds/index.ts b/src/lib/server/api/databases/seeds/index.ts similarity index 100% rename from src/lib/server/api/infrastructure/database/seeds/index.ts rename to src/lib/server/api/databases/seeds/index.ts diff --git a/src/lib/server/api/databases/seeds/roles.ts b/src/lib/server/api/databases/seeds/roles.ts new file mode 100644 index 0000000..518899c --- /dev/null +++ b/src/lib/server/api/databases/seeds/roles.ts @@ -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.') +} diff --git a/src/lib/server/api/infrastructure/database/seeds/users.ts b/src/lib/server/api/databases/seeds/users.ts similarity index 53% rename from src/lib/server/api/infrastructure/database/seeds/users.ts rename to src/lib/server/api/databases/seeds/users.ts index 15fab9a..23ae9f6 100644 --- a/src/lib/server/api/infrastructure/database/seeds/users.ts +++ b/src/lib/server/api/databases/seeds/users.ts @@ -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({ - user_id: adminUser[0].id, - type: schema.CredentialsType.PASSWORD, - secret_data: await new Argon2id().hash(`${config.ADMIN_PASSWORD}`), - }); + 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, - }); + }) }), - ); + ) }), - ); + ) } diff --git a/src/lib/server/api/infrastructure/database/tables/categories.table.ts b/src/lib/server/api/databases/tables/categories.table.ts similarity index 54% rename from src/lib/server/api/infrastructure/database/tables/categories.table.ts rename to src/lib/server/api/databases/tables/categories.table.ts index b22cfc7..eff7cc5 100644 --- a/src/lib/server/api/infrastructure/database/tables/categories.table.ts +++ b/src/lib/server/api/databases/tables/categories.table.ts @@ -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; +export type Categories = InferSelectModel export const categories_relations = relations(categoriesTable, ({ many }) => ({ categories_to_games: many(categories_to_games_table), categoriesToExternalIds: many(categoriesToExternalIdsTable), -})); +})) diff --git a/src/lib/server/api/infrastructure/database/tables/categoriesToExternalIdsTable.ts b/src/lib/server/api/databases/tables/categoriesToExternalIdsTable.ts similarity index 100% rename from src/lib/server/api/infrastructure/database/tables/categoriesToExternalIdsTable.ts rename to src/lib/server/api/databases/tables/categoriesToExternalIdsTable.ts diff --git a/src/lib/server/api/infrastructure/database/tables/categoriesToGames.ts b/src/lib/server/api/databases/tables/categoriesToGames.ts similarity index 100% rename from src/lib/server/api/infrastructure/database/tables/categoriesToGames.ts rename to src/lib/server/api/databases/tables/categoriesToGames.ts diff --git a/src/lib/server/api/infrastructure/database/tables/collectionItems.ts b/src/lib/server/api/databases/tables/collectionItems.ts similarity index 75% rename from src/lib/server/api/infrastructure/database/tables/collectionItems.ts rename to src/lib/server/api/databases/tables/collectionItems.ts index 14ea009..8466605 100644 --- a/src/lib/server/api/infrastructure/database/tables/collectionItems.ts +++ b/src/lib/server/api/databases/tables/collectionItems.ts @@ -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; +export type CollectionItems = InferSelectModel 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], }), -})); +})) diff --git a/src/lib/server/api/infrastructure/database/tables/collections.ts b/src/lib/server/api/databases/tables/collections.ts similarity index 58% rename from src/lib/server/api/infrastructure/database/tables/collections.ts rename to src/lib/server/api/databases/tables/collections.ts index 9f881ba..ed1499e 100644 --- a/src/lib/server/api/infrastructure/database/tables/collections.ts +++ b/src/lib/server/api/databases/tables/collections.ts @@ -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; +})) +export type Collections = InferSelectModel diff --git a/src/lib/server/api/infrastructure/database/tables/credentials.table.ts b/src/lib/server/api/databases/tables/credentials.table.ts similarity index 64% rename from src/lib/server/api/infrastructure/database/tables/credentials.table.ts rename to src/lib/server/api/databases/tables/credentials.table.ts index 179071d..f0204b5 100644 --- a/src/lib/server/api/infrastructure/database/tables/credentials.table.ts +++ b/src/lib/server/api/databases/tables/credentials.table.ts @@ -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; +export type Credentials = InferSelectModel diff --git a/src/lib/server/api/infrastructure/database/tables/expansions.ts b/src/lib/server/api/databases/tables/expansions.ts similarity index 65% rename from src/lib/server/api/infrastructure/database/tables/expansions.ts rename to src/lib/server/api/databases/tables/expansions.ts index 3390b88..fa6dd26 100644 --- a/src/lib/server/api/infrastructure/database/tables/expansions.ts +++ b/src/lib/server/api/databases/tables/expansions.ts @@ -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; +export type Expansions = InferSelectModel 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], }), -})); +})) diff --git a/src/lib/server/api/infrastructure/database/tables/externalIds.ts b/src/lib/server/api/databases/tables/externalIds.ts similarity index 100% rename from src/lib/server/api/infrastructure/database/tables/externalIds.ts rename to src/lib/server/api/databases/tables/externalIds.ts diff --git a/src/lib/server/api/infrastructure/database/tables/federatedIdentity.table.ts b/src/lib/server/api/databases/tables/federatedIdentity.table.ts similarity index 63% rename from src/lib/server/api/infrastructure/database/tables/federatedIdentity.table.ts rename to src/lib/server/api/databases/tables/federatedIdentity.table.ts index ea23de2..311683c 100644 --- a/src/lib/server/api/infrastructure/database/tables/federatedIdentity.table.ts +++ b/src/lib/server/api/databases/tables/federatedIdentity.table.ts @@ -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; +export type FederatedIdentity = InferSelectModel diff --git a/src/lib/server/api/infrastructure/database/tables/games.ts b/src/lib/server/api/databases/tables/games.ts similarity index 74% rename from src/lib/server/api/infrastructure/database/tables/games.ts rename to src/lib/server/api/databases/tables/games.ts index 99ec01a..3767bc2 100644 --- a/src/lib/server/api/infrastructure/database/tables/games.ts +++ b/src/lib/server/api/databases/tables/games.ts @@ -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; +export type Games = InferSelectModel diff --git a/src/lib/server/api/infrastructure/database/tables/gamesToExternalIds.ts b/src/lib/server/api/databases/tables/gamesToExternalIds.ts similarity index 100% rename from src/lib/server/api/infrastructure/database/tables/gamesToExternalIds.ts rename to src/lib/server/api/databases/tables/gamesToExternalIds.ts diff --git a/src/lib/server/api/infrastructure/database/tables/index.ts b/src/lib/server/api/databases/tables/index.ts similarity index 100% rename from src/lib/server/api/infrastructure/database/tables/index.ts rename to src/lib/server/api/databases/tables/index.ts diff --git a/src/lib/server/api/databases/tables/mechanics.ts b/src/lib/server/api/databases/tables/mechanics.ts new file mode 100644 index 0000000..b017a7b --- /dev/null +++ b/src/lib/server/api/databases/tables/mechanics.ts @@ -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 + +export const mechanics_relations = relations(mechanics, ({ many }) => ({ + mechanics_to_games: many(mechanics_to_games), + mechanicsToExternalIds: many(mechanicsToExternalIds), +})) diff --git a/src/lib/server/api/infrastructure/database/tables/mechanicsToExternalIds.ts b/src/lib/server/api/databases/tables/mechanicsToExternalIds.ts similarity index 100% rename from src/lib/server/api/infrastructure/database/tables/mechanicsToExternalIds.ts rename to src/lib/server/api/databases/tables/mechanicsToExternalIds.ts diff --git a/src/lib/server/api/infrastructure/database/tables/mechanicsToGames.ts b/src/lib/server/api/databases/tables/mechanicsToGames.ts similarity index 100% rename from src/lib/server/api/infrastructure/database/tables/mechanicsToGames.ts rename to src/lib/server/api/databases/tables/mechanicsToGames.ts diff --git a/src/lib/server/api/infrastructure/database/tables/passwordResetTokens.ts b/src/lib/server/api/databases/tables/passwordResetTokens.ts similarity index 70% rename from src/lib/server/api/infrastructure/database/tables/passwordResetTokens.ts rename to src/lib/server/api/databases/tables/passwordResetTokens.ts index 3fa94bf..9531e0e 100644 --- a/src/lib/server/api/infrastructure/database/tables/passwordResetTokens.ts +++ b/src/lib/server/api/databases/tables/passwordResetTokens.ts @@ -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; +export type PasswordResetTokens = InferSelectModel export const password_reset_token_relations = relations(password_reset_tokens, ({ one }) => ({ user: one(usersTable, { fields: [password_reset_tokens.user_id], references: [usersTable.id], }), -})); +})) diff --git a/src/lib/server/api/databases/tables/publishers.ts b/src/lib/server/api/databases/tables/publishers.ts new file mode 100644 index 0000000..e501f4c --- /dev/null +++ b/src/lib/server/api/databases/tables/publishers.ts @@ -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 + +export const publishers_relations = relations(publishers, ({ many }) => ({ + publishers_to_games: many(publishers_to_games), + publishersToExternalIds: many(publishersToExternalIds), +})) diff --git a/src/lib/server/api/infrastructure/database/tables/publishersToExternalIds.ts b/src/lib/server/api/databases/tables/publishersToExternalIds.ts similarity index 100% rename from src/lib/server/api/infrastructure/database/tables/publishersToExternalIds.ts rename to src/lib/server/api/databases/tables/publishersToExternalIds.ts diff --git a/src/lib/server/api/infrastructure/database/tables/publishersToGames.ts b/src/lib/server/api/databases/tables/publishersToGames.ts similarity index 100% rename from src/lib/server/api/infrastructure/database/tables/publishersToGames.ts rename to src/lib/server/api/databases/tables/publishersToGames.ts diff --git a/src/lib/server/api/infrastructure/database/tables/recovery-codes.table.ts b/src/lib/server/api/databases/tables/recovery-codes.table.ts similarity index 68% rename from src/lib/server/api/infrastructure/database/tables/recovery-codes.table.ts rename to src/lib/server/api/databases/tables/recovery-codes.table.ts index 80495b9..1429da0 100644 --- a/src/lib/server/api/infrastructure/database/tables/recovery-codes.table.ts +++ b/src/lib/server/api/databases/tables/recovery-codes.table.ts @@ -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; +export type RecoveryCodesTable = InferSelectModel diff --git a/src/lib/server/api/databases/tables/roles.ts b/src/lib/server/api/databases/tables/roles.ts new file mode 100644 index 0000000..0630396 --- /dev/null +++ b/src/lib/server/api/databases/tables/roles.ts @@ -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 + +export const role_relations = relations(roles, ({ many }) => ({ + user_roles: many(user_roles), +})) diff --git a/src/lib/server/api/infrastructure/database/tables/sessions.table.ts b/src/lib/server/api/databases/tables/sessions.table.ts similarity index 100% rename from src/lib/server/api/infrastructure/database/tables/sessions.table.ts rename to src/lib/server/api/databases/tables/sessions.table.ts diff --git a/src/lib/server/api/infrastructure/database/tables/two-factor.table.ts b/src/lib/server/api/databases/tables/two-factor.table.ts similarity index 68% rename from src/lib/server/api/infrastructure/database/tables/two-factor.table.ts rename to src/lib/server/api/databases/tables/two-factor.table.ts index f4cd9a2..a8ce4d7 100644 --- a/src/lib/server/api/infrastructure/database/tables/two-factor.table.ts +++ b/src/lib/server/api/databases/tables/two-factor.table.ts @@ -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; +export type TwoFactor = InferSelectModel diff --git a/src/lib/server/api/infrastructure/database/tables/userRoles.ts b/src/lib/server/api/databases/tables/userRoles.ts similarity index 68% rename from src/lib/server/api/infrastructure/database/tables/userRoles.ts rename to src/lib/server/api/databases/tables/userRoles.ts index 6d3d833..265bf88 100644 --- a/src/lib/server/api/infrastructure/database/tables/userRoles.ts +++ b/src/lib/server/api/databases/tables/userRoles.ts @@ -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; +export type UserRoles = InferSelectModel diff --git a/src/lib/server/api/infrastructure/database/tables/users.table.ts b/src/lib/server/api/databases/tables/users.table.ts similarity index 92% rename from src/lib/server/api/infrastructure/database/tables/users.table.ts rename to src/lib/server/api/databases/tables/users.table.ts index a61ca90..93850c0 100644 --- a/src/lib/server/api/infrastructure/database/tables/users.table.ts +++ b/src/lib/server/api/databases/tables/users.table.ts @@ -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', { diff --git a/src/lib/server/api/infrastructure/database/tables/wishlistItems.ts b/src/lib/server/api/databases/tables/wishlistItems.ts similarity index 68% rename from src/lib/server/api/infrastructure/database/tables/wishlistItems.ts rename to src/lib/server/api/databases/tables/wishlistItems.ts index b46ca72..e9d57c8 100644 --- a/src/lib/server/api/infrastructure/database/tables/wishlistItems.ts +++ b/src/lib/server/api/databases/tables/wishlistItems.ts @@ -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; +export type WishlistItems = InferSelectModel 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], }), -})); +})) diff --git a/src/lib/server/api/infrastructure/database/tables/wishlists.ts b/src/lib/server/api/databases/tables/wishlists.ts similarity index 58% rename from src/lib/server/api/infrastructure/database/tables/wishlists.ts rename to src/lib/server/api/databases/tables/wishlists.ts index 8e0ab5e..10dbec9 100644 --- a/src/lib/server/api/infrastructure/database/tables/wishlists.ts +++ b/src/lib/server/api/databases/tables/wishlists.ts @@ -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; +export type Wishlists = InferSelectModel export const wishlists_relations = relations(wishlists, ({ one }) => ({ user: one(usersTable, { fields: [wishlists.user_id], references: [usersTable.id], }), -})); +})) diff --git a/src/lib/server/api/dtos/create-user-role.dto.ts b/src/lib/server/api/dtos/create-user-role.dto.ts new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/lib/server/api/dtos/create-user-role.dto.ts @@ -0,0 +1 @@ + diff --git a/src/lib/server/api/dtos/id-params.dto.ts b/src/lib/server/api/dtos/id-params.dto.ts new file mode 100644 index 0000000..8a4aa50 --- /dev/null +++ b/src/lib/server/api/dtos/id-params.dto.ts @@ -0,0 +1,5 @@ +import { z } from "zod"; + +export const IdParamsDto = z.object({ + id: z.trim().number(), +}); \ No newline at end of file diff --git a/src/lib/server/api/dtos/register-emailpassword.dto.ts b/src/lib/server/api/dtos/register-emailpassword.dto.ts new file mode 100644 index 0000000..ce32b51 --- /dev/null +++ b/src/lib/server/api/dtos/register-emailpassword.dto.ts @@ -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; \ No newline at end of file diff --git a/src/lib/server/api/dtos/signin-username.dto.ts b/src/lib/server/api/dtos/signin-username.dto.ts new file mode 100644 index 0000000..0ab4a3a --- /dev/null +++ b/src/lib/server/api/dtos/signin-username.dto.ts @@ -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; \ No newline at end of file diff --git a/src/lib/server/api/dtos/signup-username-email.dto.ts b/src/lib/server/api/dtos/signup-username-email.dto.ts new file mode 100644 index 0000000..81e259e --- /dev/null +++ b/src/lib/server/api/dtos/signup-username-email.dto.ts @@ -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 diff --git a/src/lib/server/api/dtos/update-email.dto.ts b/src/lib/server/api/dtos/update-email.dto.ts new file mode 100644 index 0000000..45af50c --- /dev/null +++ b/src/lib/server/api/dtos/update-email.dto.ts @@ -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; diff --git a/src/lib/server/api/dtos/update-profile.dto.ts b/src/lib/server/api/dtos/update-profile.dto.ts new file mode 100644 index 0000000..9ea0c6f --- /dev/null +++ b/src/lib/server/api/dtos/update-profile.dto.ts @@ -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; diff --git a/src/lib/server/api/dtos/verify-password.dto.ts b/src/lib/server/api/dtos/verify-password.dto.ts new file mode 100644 index 0000000..a883135 --- /dev/null +++ b/src/lib/server/api/dtos/verify-password.dto.ts @@ -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 diff --git a/src/lib/server/api/dtos/verify-totp.dto.ts b/src/lib/server/api/dtos/verify-totp.dto.ts new file mode 100644 index 0000000..cd5f886 --- /dev/null +++ b/src/lib/server/api/dtos/verify-totp.dto.ts @@ -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; \ No newline at end of file diff --git a/src/lib/server/api/infrastructure/email-templates/email-change-notice.hbs b/src/lib/server/api/emails/email-change-notice.hbs similarity index 100% rename from src/lib/server/api/infrastructure/email-templates/email-change-notice.hbs rename to src/lib/server/api/emails/email-change-notice.hbs diff --git a/src/lib/server/api/index.ts b/src/lib/server/api/index.ts index d6a6b16..18ba883 100644 --- a/src/lib/server/api/index.ts +++ b/src/lib/server/api/index.ts @@ -1,59 +1,64 @@ 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()) - .route('/login', container.resolve(LoginController).routes()) - .route('/signup', container.resolve(SignupController).routes()) - .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' })); + .route('/me', container.resolve(IamController).routes()) + .route('/user', container.resolve(UserController).routes()) + .route('/login', container.resolve(LoginController).routes()) + .route('/signup', container.resolve(SignupController).routes()) + .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' })) + +/* -------------------------------------------------------------------------- */ +/* Cron Jobs */ +/* -------------------------------------------------------------------------- */ +container.resolve(AuthCleanupJobs).deleteStaleEmailVerificationRequests() +container.resolve(AuthCleanupJobs).deleteStaleLoginRequests() /* -------------------------------------------------------------------------- */ /* Exports */ /* -------------------------------------------------------------------------- */ -export type AppType = typeof routes; - -export const rpc = hc(config.ORIGIN); -export type ApiClient = typeof rpc; -export type ApiRoutes = typeof routes; -export { app }; \ No newline at end of file +export const rpc = hc(config.ORIGIN) +export type ApiClient = typeof rpc +export type ApiRoutes = typeof routes diff --git a/src/lib/server/api/infrastructure/database/migrate.ts b/src/lib/server/api/infrastructure/database/migrate.ts deleted file mode 100644 index 156ac34..0000000 --- a/src/lib/server/api/infrastructure/database/migrate.ts +++ /dev/null @@ -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(); diff --git a/src/lib/server/api/infrastructure/database/seeds/roles.ts b/src/lib/server/api/infrastructure/database/seeds/roles.ts deleted file mode 100644 index 20741ea..0000000 --- a/src/lib/server/api/infrastructure/database/seeds/roles.ts +++ /dev/null @@ -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.'); -} diff --git a/src/lib/server/api/infrastructure/database/tables/mechanics.ts b/src/lib/server/api/infrastructure/database/tables/mechanics.ts deleted file mode 100644 index 2f489bc..0000000 --- a/src/lib/server/api/infrastructure/database/tables/mechanics.ts +++ /dev/null @@ -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; - -export const mechanics_relations = relations(mechanics, ({ many }) => ({ - mechanics_to_games: many(mechanics_to_games), - mechanicsToExternalIds: many(mechanicsToExternalIds), -})); diff --git a/src/lib/server/api/infrastructure/database/tables/publishers.ts b/src/lib/server/api/infrastructure/database/tables/publishers.ts deleted file mode 100644 index 48d0e66..0000000 --- a/src/lib/server/api/infrastructure/database/tables/publishers.ts +++ /dev/null @@ -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; - -export const publishers_relations = relations(publishers, ({ many }) => ({ - publishers_to_games: many(publishers_to_games), - publishersToExternalIds: many(publishersToExternalIds), -})); diff --git a/src/lib/server/api/infrastructure/database/tables/roles.ts b/src/lib/server/api/infrastructure/database/tables/roles.ts deleted file mode 100644 index 4e701ac..0000000 --- a/src/lib/server/api/infrastructure/database/tables/roles.ts +++ /dev/null @@ -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; - -export const role_relations = relations(roles, ({ many }) => ({ - user_roles: many(user_roles), -})); diff --git a/src/lib/server/api/infrastructure/database/utils.ts b/src/lib/server/api/infrastructure/database/utils.ts deleted file mode 100644 index 5b926ef..0000000 --- a/src/lib/server/api/infrastructure/database/utils.ts +++ /dev/null @@ -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 = (values: T[]): T | null => { - if (values.length === 0) return null; - return values[0]!; -}; - -export const takeFirstOrThrow = (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() -}; diff --git a/src/lib/server/api/interfaces/controller.interface.ts b/src/lib/server/api/interfaces/controller.interface.ts deleted file mode 100644 index 852e695..0000000 --- a/src/lib/server/api/interfaces/controller.interface.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Hono } from 'hono'; -import type { HonoTypes } from '../types'; -import type { BlankSchema } from 'hono/types'; - -export interface Controller { - controller: Hono; - routes(): any; -} diff --git a/src/lib/server/api/jobs/auth-cleanup.job.ts b/src/lib/server/api/jobs/auth-cleanup.job.ts new file mode 100644 index 0000000..fb2ec6c --- /dev/null +++ b/src/lib/server/api/jobs/auth-cleanup.job.ts @@ -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 + } + }) + } +} diff --git a/src/lib/server/api/middleware/auth.middleware.ts b/src/lib/server/api/middleware/auth.middleware.ts index bd2f4c9..5d2eab7 100644 --- a/src/lib/server/api/middleware/auth.middleware.ts +++ b/src/lib/server/api/middleware/auth.middleware.ts @@ -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 = createMiddleware(async (c, next) => { if (c.req.method === 'GET') { diff --git a/src/lib/server/api/middleware/rate-limiter.middleware.ts b/src/lib/server/api/middleware/rate-limiter.middleware.ts index d704571..9b49d10 100644 --- a/src/lib/server/api/middleware/rate-limiter.middleware.ts +++ b/src/lib/server/api/middleware/rate-limiter.middleware.ts @@ -1,32 +1,34 @@ -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 - 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; - return `${clientKey}_${pathKey}` - }, // Method to generate custom identifiers for clients. - // Redis store configuration - store: new RedisStore({ - // @ts-expect-error - Known issue: the `call` function is not present in @types/ioredis - sendCommand: (...args: string[]) => client.call(...args), - }) as any, - }) + 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 + 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 + return `${clientKey}_${pathKey}` + }, // Method to generate custom identifiers for clients. + // Redis store configuration + store: new RedisStore({ + // @ts-expect-error - Known issue: the `call` function is not present in @types/ioredis + sendCommand: (...args: string[]) => client.call(...args), + }) as any, + }) } - - diff --git a/src/lib/server/api/infrastructure/database/index.ts b/src/lib/server/api/packages/drizzle.ts similarity index 68% rename from src/lib/server/api/infrastructure/database/index.ts rename to src/lib/server/api/packages/drizzle.ts index 556e598..474ce89 100644 --- a/src/lib/server/api/infrastructure/database/index.ts +++ b/src/lib/server/api/packages/drizzle.ts @@ -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 diff --git a/src/lib/server/api/infrastructure/auth/lucia.ts b/src/lib/server/api/packages/lucia.ts similarity index 66% rename from src/lib/server/api/infrastructure/auth/lucia.ts rename to src/lib/server/api/packages/lucia.ts index 8141cb6..825e478 100644 --- a/src/lib/server/api/infrastructure/auth/lucia.ts +++ b/src/lib/server/api/packages/lucia.ts @@ -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 } } diff --git a/src/lib/server/api/providers/database.provider.ts b/src/lib/server/api/providers/database.provider.ts index c7da118..df2ed64 100644 --- a/src/lib/server/api/providers/database.provider.ts +++ b/src/lib/server/api/providers/database.provider.ts @@ -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, { useValue: db }); +container.register(DatabaseProvider, { useValue: db }) diff --git a/src/lib/server/api/providers/index.ts b/src/lib/server/api/providers/index.ts deleted file mode 100644 index 0ac960b..0000000 --- a/src/lib/server/api/providers/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './database.provider'; -export * from './lucia.provider'; -export * from './redis.provider'; diff --git a/src/lib/server/api/providers/lucia.provider.ts b/src/lib/server/api/providers/lucia.provider.ts index 6546d43..4291977 100644 --- a/src/lib/server/api/providers/lucia.provider.ts +++ b/src/lib/server/api/providers/lucia.provider.ts @@ -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, { useValue: lucia }); +container.register(LuciaProvider, { useValue: lucia }) diff --git a/src/lib/server/api/providers/redis.provider.ts b/src/lib/server/api/providers/redis.provider.ts index 26496de..cf7ec0d 100644 --- a/src/lib/server/api/providers/redis.provider.ts +++ b/src/lib/server/api/providers/redis.provider.ts @@ -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, { - useValue: new RedisClient(config.REDIS_URL) -}); + useValue: new RedisClient(config.REDIS_URL, { + maxRetriesPerRequest: null, + }), +}) diff --git a/src/lib/server/api/repositories/collections.repository.ts b/src/lib/server/api/repositories/collections.repository.ts index 72db342..fa5669d 100644 --- a/src/lib/server/api/repositories/collections.repository.ts +++ b/src/lib/server/api/repositories/collections.repository.ts @@ -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; -export type UpdateCollection = Partial; +export type CreateCollection = InferInsertModel +export type UpdateCollection = Partial @injectable() -export class CollectionsRepository { - constructor(@inject(DatabaseProvider) private readonly db: DatabaseProvider) { } +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) } -} \ No newline at end of file + + trxHost(trx: DatabaseProvider) { + return new CollectionsRepository(trx) + } +} diff --git a/src/lib/server/api/repositories/credentials.repository.ts b/src/lib/server/api/repositories/credentials.repository.ts index 2594af5..7042384 100644 --- a/src/lib/server/api/repositories/credentials.repository.ts +++ b/src/lib/server/api/repositories/credentials.repository.ts @@ -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 export type UpdateCredentials = Partial @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) + } } diff --git a/src/lib/server/api/repositories/roles.repository.ts b/src/lib/server/api/repositories/roles.repository.ts index b1ab846..e940f4b 100644 --- a/src/lib/server/api/repositories/roles.repository.ts +++ b/src/lib/server/api/repositories/roles.repository.ts @@ -1,8 +1,9 @@ -import { eq, type InferInsertModel } from 'drizzle-orm'; -import { takeFirstOrThrow } from '../infrastructure/database/utils'; -import {roles} from "$lib/server/api/infrastructure/database/tables"; -import {inject, injectable} from "tsyringe"; -import {DatabaseProvider} from "$lib/server/api/providers"; +import type { Repository } from '$lib/server/api/common/interfaces/repository.interface' +import { DatabaseProvider } from '$lib/server/api/providers/database.provider' +import { type InferInsertModel, eq } from 'drizzle-orm' +import { inject, injectable } from 'tsyringe' +import { takeFirstOrThrow } from '../common/utils/repository.utils' +import { roles } from '../databases/tables' /* -------------------------------------------------------------------------- */ /* Repository */ @@ -20,59 +21,54 @@ storing data. They should not contain any business logic, only database queries. In our case the method 'trxHost' is used to set the transaction context. */ -export type CreateRole = InferInsertModel; -export type UpdateRole = Partial; +export type CreateRole = InferInsertModel +export type UpdateRole = Partial @injectable() -export class RolesRepository { - constructor(@inject(DatabaseProvider) private readonly db: DatabaseProvider) { } +export class RolesRepository implements Repository { + constructor(@inject(DatabaseProvider) private readonly db: DatabaseProvider) {} async findOneById(id: string) { return this.db.query.roles.findFirst({ - where: eq(roles.id, id) - }); + where: eq(roles.id, id), + }) } async findOneByIdOrThrow(id: string) { - const role = await this.findOneById(id); - if (!role) throw Error('Role not found'); - return role; + const role = await this.findOneById(id) + if (!role) throw Error('Role not found') + return role } async findAll() { - return this.db.query.roles.findMany(); + return this.db.query.roles.findMany() } async findOneByName(name: string) { return this.db.query.roles.findFirst({ - where: eq(roles.name, name) - }); + where: eq(roles.name, name), + }) } async findOneByNameOrThrow(name: string) { - const role = await this.findOneByName(name); - if (!role) throw Error('Role not found'); - return role; + const role = await this.findOneByName(name) + if (!role) throw Error('Role not found') + return role } async create(data: CreateRole) { - return this.db.insert(roles).values(data).returning().then(takeFirstOrThrow); + return this.db.insert(roles).values(data).returning().then(takeFirstOrThrow) } async update(id: string, data: UpdateRole) { - return this.db - .update(roles) - .set(data) - .where(eq(roles.id, id)) - .returning() - .then(takeFirstOrThrow); + return this.db.update(roles).set(data).where(eq(roles.id, id)).returning().then(takeFirstOrThrow) } async delete(id: string) { - return this.db - .delete(roles) - .where(eq(roles.id, id)) - .returning() - .then(takeFirstOrThrow); + return this.db.delete(roles).where(eq(roles.id, id)).returning().then(takeFirstOrThrow) + } + + trxHost(trx: DatabaseProvider) { + return new RolesRepository(trx) } } diff --git a/src/lib/server/api/repositories/user_roles.repository.ts b/src/lib/server/api/repositories/user_roles.repository.ts index cbc688f..90e2823 100644 --- a/src/lib/server/api/repositories/user_roles.repository.ts +++ b/src/lib/server/api/repositories/user_roles.repository.ts @@ -1,8 +1,9 @@ -import { eq, type InferInsertModel } from 'drizzle-orm'; -import { takeFirstOrThrow } from '../infrastructure/database/utils'; -import {user_roles} from "$lib/server/api/infrastructure/database/tables"; -import {inject, injectable} from "tsyringe"; -import {DatabaseProvider} from "$lib/server/api/providers"; +import type { Repository } from '$lib/server/api/common/interfaces/repository.interface' +import { DatabaseProvider } from '$lib/server/api/providers/database.provider' +import { type InferInsertModel, eq } from 'drizzle-orm' +import { inject, injectable } from 'tsyringe' +import { takeFirstOrThrow } from '../common/utils/repository.utils' +import { user_roles } from '../databases/tables' /* -------------------------------------------------------------------------- */ /* Repository */ @@ -20,40 +21,40 @@ storing data. They should not contain any business logic, only database queries. In our case the method 'trxHost' is used to set the transaction context. */ -export type CreateUserRole = InferInsertModel; -export type UpdateUserRole = Partial; +export type CreateUserRole = InferInsertModel +export type UpdateUserRole = Partial @injectable() -export class UserRolesRepository { - constructor(@inject(DatabaseProvider) private readonly db: DatabaseProvider) { } +export class UserRolesRepository implements Repository { + constructor(@inject(DatabaseProvider) private readonly db: DatabaseProvider) {} async findOneById(id: string) { return this.db.query.user_roles.findFirst({ - where: eq(user_roles.id, id) - }); + where: eq(user_roles.id, id), + }) } async findOneByIdOrThrow(id: string) { - const userRole = await this.findOneById(id); - if (!userRole) throw Error('User not found'); - return userRole; + const userRole = await this.findOneById(id) + if (!userRole) throw Error('User not found') + return userRole } async findAllByUserId(userId: string) { return this.db.query.user_roles.findMany({ - where: eq(user_roles.user_id, userId) - }); + where: eq(user_roles.user_id, userId), + }) } async create(data: CreateUserRole) { - return this.db.insert(user_roles).values(data).returning().then(takeFirstOrThrow); + return this.db.insert(user_roles).values(data).returning().then(takeFirstOrThrow) } async delete(id: string) { - return this.db - .delete(user_roles) - .where(eq(user_roles.id, id)) - .returning() - .then(takeFirstOrThrow); + return this.db.delete(user_roles).where(eq(user_roles.id, id)).returning().then(takeFirstOrThrow) + } + + trxHost(trx: DatabaseProvider) { + return new UserRolesRepository(trx) } } diff --git a/src/lib/server/api/repositories/users.repository.ts b/src/lib/server/api/repositories/users.repository.ts index e65ec7a..b44b1da 100644 --- a/src/lib/server/api/repositories/users.repository.ts +++ b/src/lib/server/api/repositories/users.repository.ts @@ -1,8 +1,9 @@ -import { eq, type InferInsertModel } from 'drizzle-orm'; -import { usersTable } from '../infrastructure/database/tables/users.table'; -import { takeFirstOrThrow } from '../infrastructure/database/utils'; -import {inject, injectable} from "tsyringe"; -import {DatabaseProvider} from "$lib/server/api/providers"; +import type { Repository } from '$lib/server/api/common/interfaces/repository.interface' +import { usersTable } from '$lib/server/api/databases/tables/users.table' +import { DatabaseProvider } from '$lib/server/api/providers/database.provider' +import { type InferInsertModel, eq } from 'drizzle-orm' +import { inject, injectable } from 'tsyringe' +import { takeFirstOrThrow } from '../common/utils/repository.utils' /* -------------------------------------------------------------------------- */ /* Repository */ @@ -20,55 +21,50 @@ storing data. They should not contain any business logic, only database queries. In our case the method 'trxHost' is used to set the transaction context. */ -export type CreateUser = InferInsertModel; -export type UpdateUser = Partial; +export type CreateUser = InferInsertModel +export type UpdateUser = Partial @injectable() -export class UsersRepository { - constructor(@inject(DatabaseProvider) private readonly db: DatabaseProvider) { } +export class UsersRepository implements Repository { + constructor(@inject(DatabaseProvider) private readonly db: DatabaseProvider) {} async findOneById(id: string) { return this.db.query.usersTable.findFirst({ - where: eq(usersTable.id, id) - }); + where: eq(usersTable.id, id), + }) } async findOneByIdOrThrow(id: string) { - const user = await this.findOneById(id); - if (!user) throw Error('User not found'); - return user; + const user = await this.findOneById(id) + if (!user) throw Error('User not found') + return user } async findOneByUsername(username: string) { return this.db.query.usersTable.findFirst({ - where: eq(usersTable.username, username) - }); + where: eq(usersTable.username, username), + }) } async findOneByEmail(email: string) { return this.db.query.usersTable.findFirst({ - where: eq(usersTable.email, email) - }); + where: eq(usersTable.email, email), + }) } async create(data: CreateUser) { - return this.db.insert(usersTable).values(data).returning().then(takeFirstOrThrow); + return this.db.insert(usersTable).values(data).returning().then(takeFirstOrThrow) } async update(id: string, data: UpdateUser) { - return this.db - .update(usersTable) - .set(data) - .where(eq(usersTable.id, id)) - .returning() - .then(takeFirstOrThrow); + return this.db.update(usersTable).set(data).where(eq(usersTable.id, id)).returning().then(takeFirstOrThrow) } async delete(id: string) { - return this.db - .delete(usersTable) - .where(eq(usersTable.id, id)) - .returning() - .then(takeFirstOrThrow); + return this.db.delete(usersTable).where(eq(usersTable.id, id)).returning().then(takeFirstOrThrow) + } + + trxHost(trx: DatabaseProvider) { + return new UsersRepository(trx) } } diff --git a/src/lib/server/api/repositories/wishlists.repository.ts b/src/lib/server/api/repositories/wishlists.repository.ts index 4ea4076..00c074b 100644 --- a/src/lib/server/api/repositories/wishlists.repository.ts +++ b/src/lib/server/api/repositories/wishlists.repository.ts @@ -1,18 +1,19 @@ -import {inject, injectable} from "tsyringe"; -import {DatabaseProvider} from "$lib/server/api/providers"; -import { eq, type InferInsertModel } from "drizzle-orm"; -import { wishlists } from "../infrastructure/database/tables"; -import { takeFirstOrThrow } from "../infrastructure/database/utils"; +import type { Repository } from '$lib/server/api/common/interfaces/repository.interface' +import { DatabaseProvider } from '$lib/server/api/providers/database.provider' +import { type InferInsertModel, eq } from 'drizzle-orm' +import { inject, injectable } from 'tsyringe' +import { takeFirstOrThrow } from '../common/utils/repository.utils' +import { wishlists } from '../databases/tables' -export type CreateWishlist = InferInsertModel; -export type UpdateWishlist = Partial; +export type CreateWishlist = InferInsertModel +export type UpdateWishlist = Partial @injectable() -export class WishlistsRepository { - constructor(@inject(DatabaseProvider) private readonly db: DatabaseProvider){ } +export class WishlistsRepository implements Repository { + constructor(@inject(DatabaseProvider) private readonly db: DatabaseProvider) {} async findAll() { - return this.db.query.wishlists.findMany(); + return this.db.query.wishlists.findMany() } async findOneById(id: string) { @@ -20,8 +21,8 @@ export class WishlistsRepository { where: eq(wishlists.id, id), columns: { cuid: true, - name: true - } + name: true, + }, }) } @@ -30,8 +31,8 @@ export class WishlistsRepository { where: eq(wishlists.cuid, cuid), columns: { cuid: true, - name: true - } + name: true, + }, }) } @@ -40,8 +41,8 @@ export class WishlistsRepository { where: eq(wishlists.user_id, userId), columns: { cuid: true, - name: true - } + name: true, + }, }) } @@ -50,21 +51,20 @@ export class WishlistsRepository { where: eq(wishlists.user_id, userId), columns: { cuid: true, - name: true - } + name: true, + }, }) } async create(data: CreateWishlist) { - return this.db.insert(wishlists).values(data).returning().then(takeFirstOrThrow); + return this.db.insert(wishlists).values(data).returning().then(takeFirstOrThrow) } async update(id: string, data: UpdateWishlist) { - return this.db - .update(wishlists) - .set(data) - .where(eq(wishlists.id, id)) - .returning() - .then(takeFirstOrThrow); + return this.db.update(wishlists).set(data).where(eq(wishlists.id, id)).returning().then(takeFirstOrThrow) } -} \ No newline at end of file + + trxHost(trx: DatabaseProvider) { + return new WishlistsRepository(trx) + } +} diff --git a/src/lib/server/api/services/iam.service.ts b/src/lib/server/api/services/iam.service.ts index 16a8cba..f7b3599 100644 --- a/src/lib/server/api/services/iam.service.ts +++ b/src/lib/server/api/services/iam.service.ts @@ -1,8 +1,9 @@ -import type { UpdateEmailDto } from "$lib/dtos/update-email.dto"; -import type { UpdateProfileDto } from "$lib/dtos/update-profile.dto"; -import { UsersService } from "$lib/server/api/services/users.service"; -import { inject, injectable } from 'tsyringe'; -import { LuciaProvider } from '$lib/server/api/providers'; +import type { UpdateEmailDto } from '$lib/server/api/dtos/update-email.dto' +import type { UpdateProfileDto } from '$lib/server/api/dtos/update-profile.dto' +import type { VerifyPasswordDto } from '$lib/server/api/dtos/verify-password.dto' +import { LuciaProvider } from '$lib/server/api/providers/lucia.provider' +import { UsersService } from '$lib/server/api/services/users.service' +import { inject, injectable } from 'tsyringe' /* -------------------------------------------------------------------------- */ /* Service */ @@ -25,54 +26,54 @@ simple as possible. This makes the service easier to read, test and understand. export class IamService { constructor( @inject(LuciaProvider) private readonly lucia: LuciaProvider, - @inject(UsersService) private readonly usersService: UsersService - ) { } + @inject(UsersService) private readonly usersService: UsersService, + ) {} async logout(sessionId: string) { - return this.lucia.invalidateSession(sessionId); + return this.lucia.invalidateSession(sessionId) } async updateProfile(userId: string, data: UpdateProfileDto) { - const user = await this.usersService.findOneById(userId); + const user = await this.usersService.findOneById(userId) if (!user) { return { - error: 'User not found' - }; + error: 'User not found', + } } - const existingUserForNewUsername = await this.usersService.findOneByUsername(data.username); + const existingUserForNewUsername = await this.usersService.findOneByUsername(data.username) if (existingUserForNewUsername && existingUserForNewUsername.id !== userId) { return { - error: 'Username already in use' - }; + error: 'Username already in use', + } } return this.usersService.updateUser(userId, { first_name: data.firstName, last_name: data.lastName, - username: data.username !== user.username ? data.username : user.username - }); + username: data.username !== user.username ? data.username : user.username, + }) } async updateEmail(userId: string, data: UpdateEmailDto) { - const { email } = data; + const { email } = data - const existingUserEmail = await this.usersService.findOneByEmail(email); + const existingUserEmail = await this.usersService.findOneByEmail(email) if (existingUserEmail && existingUserEmail.id !== userId) { - return null; + return null } return this.usersService.updateUser(userId, { email, - }); + }) } async verifyPassword(userId: string, data: VerifyPasswordDto) { - const user = await this.usersService.findOneById(userId); + const user = await this.usersService.findOneById(userId) if (!user) { - return null; + return null } - const { password } = data; - return this.usersService.verifyPassword(userId, { password }); + const { password } = data + return this.usersService.verifyPassword(userId, { password }) } } diff --git a/src/lib/server/api/services/jobs.service.ts b/src/lib/server/api/services/jobs.service.ts new file mode 100644 index 0000000..4f5f0d1 --- /dev/null +++ b/src/lib/server/api/services/jobs.service.ts @@ -0,0 +1,16 @@ +import { RedisProvider } from '$lib/server/api/providers/redis.provider' +import { type Processor, Queue, Worker } from 'bullmq' +import { inject, injectable } from 'tsyringe' + +@injectable() +export class JobsService { + constructor(@inject(RedisProvider) private readonly redis: RedisProvider) {} + + createQueue(name: string) { + return new Queue(name, { connection: this.redis }) + } + + createWorker(name: string, processor: Processor) { + return new Worker(name, processor, { connection: this.redis }) + } +} diff --git a/src/lib/server/api/services/loginrequest.service.ts b/src/lib/server/api/services/loginrequest.service.ts index d3f9c8d..6748eb8 100644 --- a/src/lib/server/api/services/loginrequest.service.ts +++ b/src/lib/server/api/services/loginrequest.service.ts @@ -1,98 +1,95 @@ -import { inject, injectable } from 'tsyringe'; -import { BadRequest } from '../common/errors'; -import { DatabaseProvider } from '../providers'; -import { MailerService } from './mailer.service'; -import { TokensService } from './tokens.service'; -import { LuciaProvider } from '../providers/lucia.provider'; -import { UsersRepository } from '../repositories/users.repository'; -import { CredentialsRepository } from '../repositories/credentials.repository'; -import type { HonoRequest } from 'hono'; -import type {SigninUsernameDto} from "$lib/dtos/signin-username.dto"; -import type {Credentials} from "$lib/server/api/infrastructure/database/tables"; +import type { SigninUsernameDto } from '$lib/server/api/dtos/signin-username.dto' +import type { HonoRequest } from 'hono' +import { inject, injectable } from 'tsyringe' +import { BadRequest } from '../common/exceptions' +import type { Credentials } from '../databases/tables' +import { DatabaseProvider } from '../providers/database.provider' +import { LuciaProvider } from '../providers/lucia.provider' +import { CredentialsRepository } from '../repositories/credentials.repository' +import { UsersRepository } from '../repositories/users.repository' +import { MailerService } from './mailer.service' +import { TokensService } from './tokens.service' @injectable() export class LoginRequestsService { - constructor( - @inject(LuciaProvider) private readonly lucia: LuciaProvider, - @inject(DatabaseProvider) private readonly db: DatabaseProvider, - @inject(TokensService) private readonly tokensService: TokensService, - @inject(MailerService) private readonly mailerService: MailerService, - @inject(UsersRepository) private readonly usersRepository: UsersRepository, - @inject(CredentialsRepository) private readonly credentialsRepository: CredentialsRepository, - ) { } + constructor( + @inject(LuciaProvider) private readonly lucia: LuciaProvider, + @inject(DatabaseProvider) private readonly db: DatabaseProvider, + @inject(TokensService) private readonly tokensService: TokensService, + @inject(MailerService) private readonly mailerService: MailerService, + @inject(UsersRepository) private readonly usersRepository: UsersRepository, + @inject(CredentialsRepository) private readonly credentialsRepository: CredentialsRepository, + ) {} - // async create(data: RegisterEmailDto) { - // // generate a token, expiry date, and hash - // const { token, expiry, hashedToken } = await this.tokensService.generateTokenWithExpiryAndHash(15, 'm'); - // // save the login request to the database - ensuring we save the hashedToken - // await this.loginRequestsRepository.create({ email: data.email, hashedToken, expiresAt: expiry }); - // // send the login request email - // await this.mailerService.sendLoginRequest({ - // to: data.email, - // props: { token: token } - // }); - // } + // async create(data: RegisterEmailDto) { + // // generate a token, expiry date, and hash + // const { token, expiry, hashedToken } = await this.tokensService.generateTokenWithExpiryAndHash(15, 'm'); + // // save the login request to the database - ensuring we save the hashedToken + // await this.loginRequestsRepository.create({ email: data.email, hashedToken, expiresAt: expiry }); + // // send the login request email + // await this.mailerService.sendLoginRequest({ + // to: data.email, + // props: { token: token } + // }); + // } - async verify(data: SigninUsernameDto, req: HonoRequest) { - const requestIpAddress = req.header('x-real-ip'); - const requestIpCountry = req.header('x-vercel-ip-country'); - const existingUser = await this.usersRepository.findOneByUsername(data.username); + async verify(data: SigninUsernameDto, req: HonoRequest) { + const requestIpAddress = req.header('x-real-ip') + const requestIpCountry = req.header('x-vercel-ip-country') + const existingUser = await this.usersRepository.findOneByUsername(data.username) - if (!existingUser) { - throw BadRequest('User not found'); + if (!existingUser) { + throw BadRequest('User not found') } - const credential = await this.credentialsRepository.findPasswordCredentialsByUserId(existingUser.id); + const credential = await this.credentialsRepository.findPasswordCredentialsByUserId(existingUser.id) - if (!credential) { - throw BadRequest('Invalid credentials'); - } + if (!credential) { + throw BadRequest('Invalid credentials') + } - if (!await this.tokensService.verifyHashedToken(credential.secret_data, data.password)) { - throw BadRequest('Invalid credentials'); - } + if (!(await this.tokensService.verifyHashedToken(credential.secret_data, data.password))) { + throw BadRequest('Invalid credentials') + } - const totpCredentials = await this.credentialsRepository.findTOTPCredentialsByUserId(existingUser.id); + const totpCredentials = await this.credentialsRepository.findTOTPCredentialsByUserId(existingUser.id) - return await this.createUserSession(existingUser.id, req, totpCredentials); - } + return await this.createUserSession(existingUser.id, req, totpCredentials) + } async createUserSession(existingUserId: string, req: HonoRequest, totpCredentials: Credentials | undefined) { - const requestIpAddress = req.header('x-real-ip'); - const requestIpCountry = req.header('x-vercel-ip-country'); + const requestIpAddress = req.header('x-real-ip') + const requestIpCountry = req.header('x-vercel-ip-country') return this.lucia.createSession(existingUserId, { ip_country: requestIpCountry || 'unknown', ip_address: requestIpAddress || 'unknown', - twoFactorAuthEnabled: - !!totpCredentials && - totpCredentials?.secret_data !== null && - totpCredentials?.secret_data !== '', + twoFactorAuthEnabled: !!totpCredentials && totpCredentials?.secret_data !== null && totpCredentials?.secret_data !== '', isTwoFactorAuthenticated: false, - }); + }) } - // Create a new user and send a welcome email - or other onboarding process - private async handleNewUserRegistration(email: string) { - const newUser = await this.usersRepository.create({ email, verified: true }) - this.mailerService.sendWelcome({ to: email, props: null }); - // TODO: add whatever onboarding process or extra data you need here - return newUser - } + // Create a new user and send a welcome email - or other onboarding process + private async handleNewUserRegistration(email: string) { + const newUser = await this.usersRepository.create({ email, verified: true }) + this.mailerService.sendWelcome({ to: email, props: null }) + // TODO: add whatever onboarding process or extra data you need here + return newUser + } - // Fetch a valid request from the database, verify the token and burn the request if it is valid - // private async fetchValidRequest(email: string, token: string) { - // return await this.db.transaction(async (trx) => { - // // fetch the login request - // const loginRequest = await this.loginRequestsRepository.trxHost(trx).findOneByEmail(email) - // if (!loginRequest) return null; + // Fetch a valid request from the database, verify the token and burn the request if it is valid + // private async fetchValidRequest(email: string, token: string) { + // return await this.db.transaction(async (trx) => { + // // fetch the login request + // const loginRequest = await this.loginRequestsRepository.trxHost(trx).findOneByEmail(email) + // if (!loginRequest) return null; - // // check if the token is valid - // const isValidRequest = await this.tokensService.verifyHashedToken(loginRequest.hashedToken, token); - // if (!isValidRequest) return null + // // check if the token is valid + // const isValidRequest = await this.tokensService.verifyHashedToken(loginRequest.hashedToken, token); + // if (!isValidRequest) return null - // // if the token is valid, burn the request - // await this.loginRequestsRepository.trxHost(trx).deleteById(loginRequest.id); - // return loginRequest - // }) - // } -} \ No newline at end of file + // // if the token is valid, burn the request + // await this.loginRequestsRepository.trxHost(trx).deleteById(loginRequest.id); + // return loginRequest + // }) + // } +} diff --git a/src/lib/server/api/services/mailer.service.ts b/src/lib/server/api/services/mailer.service.ts index f644f11..c780275 100644 --- a/src/lib/server/api/services/mailer.service.ts +++ b/src/lib/server/api/services/mailer.service.ts @@ -1,104 +1,42 @@ -import fs from 'fs'; -import path from 'path'; -import nodemailer from 'nodemailer'; -import handlebars from 'handlebars'; -import { fileURLToPath } from 'url'; -import { injectable } from 'tsyringe'; +import { injectable } from 'tsyringe' +import type { Email } from '../common/inferfaces/email.interface' +import { config } from '../configs/config' -/* -------------------------------------------------------------------------- */ -/* Service */ -/* -------------------------------------------------------------------------- */ -/* -------------------------------------------------------------------------- */ -/* ---------------------------------- About --------------------------------- */ -/* -Services are responsible for handling business logic and data manipulation. -They genreally call on repositories or other services to complete a use-case. -*/ -/* ---------------------------------- Notes --------------------------------- */ -/* -Services should be kept as clean and simple as possible. - -Create private functions to handle complex logic and keep the public methods as -simple as possible. This makes the service easier to read, test and understand. -*/ -/* -------------------------------------------------------------------------- */ - -type SendMail = { - to: string | string[]; - subject: string; - html: string; -}; - -type SendTemplate = { - to: string | string[]; - props: T; -}; +type SendProps = { + to: string | string[] + email: Email +} @injectable() export class MailerService { - private nodemailer = nodemailer.createTransport({ - host: 'smtp.ethereal.email', - port: 587, - secure: false, // Use `true` for port 465, `false` for all other ports - auth: { - user: 'adella.hoppe@ethereal.email', - pass: 'dshNQZYhATsdJ3ENke' + async send(data: SendProps) { + const mailer = config.isProduction ? this.sendProd : this.sendDev + await mailer(data) + } + + private async sendDev({ to, email }: SendProps) { + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + Attachments: [], + From: { Email: 'noreply@tofustack.com', Name: 'TofuStack' }, + HTML: email.html(), + Subject: email.subject(), + Text: email.html(), + To: Array.isArray(to) ? to.map((to) => ({ Email: to, Name: to })) : [{ Email: to, Name: to }], + }), } - }); - sendEmailVerificationToken(data: SendTemplate<{ token: string }>) { - const template = handlebars.compile(this.getTemplate('email-verification-token')); - return this.send({ - to: data.to, - subject: 'Email Verification', - html: template({ token: data.props.token }) - }); + const response = await fetch('http://localhost:8025/api/v1/send', options) + const data = await response.json() + console.log(`http://localhost:8025/view/${data.ID}`) } - sendEmailChangeNotification(data: SendTemplate) { - const template = handlebars.compile(this.getTemplate('email-change-notice')); - return this.send({ - to: data.to, - subject: 'Email Change Notice', - html: template(null) - }); - } - - sendLoginRequest(data: SendTemplate<{ token: string }>) { - const template = handlebars.compile(this.getTemplate('email-verification-token')); - return this.send({ - to: data.to, - subject: 'Login Request', - html: template({ token: data.props.token }) - }); - } - - sendWelcome(data: SendTemplate) { - const template = handlebars.compile(this.getTemplate('welcome')); - return this.send({ - to: data.to, - subject: 'Welcome!', - html: template(null) - }); - } - - private async send({ to, subject, html }: SendMail) { - const message = await this.nodemailer.sendMail({ - from: '"Example" ', // sender address - bcc: to, - subject, // Subject line - text: html, - html - }); - console.log(nodemailer.getTestMessageUrl(message)); - } - - private getTemplate(template: string) { - const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file - const __dirname = path.dirname(__filename); // get the name of the directory - return fs.readFileSync( - path.join(__dirname, `../infrastructure/email-templates/${template}.hbs`), - 'utf-8' - ); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private async sendProd({ to, email }: SendProps) { + // CONFIGURE MAILER } } diff --git a/src/lib/server/api/services/queues.service.ts b/src/lib/server/api/services/queues.service.ts deleted file mode 100644 index 97e7ca3..0000000 --- a/src/lib/server/api/services/queues.service.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { injectable } from "tsyringe"; -import RedisClient from 'ioredis' -import { config } from "../common/config"; -import { Queue, Worker, type Processor } from 'bullmq'; - -@injectable() -export class QueuesServices { - connection = new RedisClient(config.REDIS_URL); - - constructor() { } - - createQueue(name: string) { - return new Queue(name, { connection: this.connection }) - } - - createWorker(name: string, prcoessor: Processor) { - return new Worker(name, prcoessor, { connection: this.connection }) - } -} \ No newline at end of file diff --git a/src/lib/server/api/services/totp.service.ts b/src/lib/server/api/services/totp.service.ts index f75674e..b8c5fbf 100644 --- a/src/lib/server/api/services/totp.service.ts +++ b/src/lib/server/api/services/totp.service.ts @@ -1,47 +1,43 @@ -import { inject, injectable } from "tsyringe"; -import { HMAC } from 'oslo/crypto'; -import { decodeHex, encodeHex } from 'oslo/encoding'; -import {CredentialsRepository} from "$lib/server/api/repositories/credentials.repository"; -import { TOTPController } from "oslo/otp"; -import type { CredentialsType } from "$db/tables"; +import { CredentialsRepository } from '$lib/server/api/repositories/credentials.repository' +import { HMAC } from 'oslo/crypto' +import { decodeHex, encodeHex } from 'oslo/encoding' +import { TOTPController } from 'oslo/otp' +import { inject, injectable } from 'tsyringe' +import type { CredentialsType } from '../databases/tables' @injectable() export class TotpService { - - constructor( - @inject(CredentialsRepository) private readonly credentialsRepository: CredentialsRepository - ) { - } + constructor(@inject(CredentialsRepository) private readonly credentialsRepository: CredentialsRepository) {} async findOneByUserId(userId: string) { - return this.credentialsRepository.findTOTPCredentialsByUserId(userId); + return this.credentialsRepository.findTOTPCredentialsByUserId(userId) } async findOneByUserIdOrThrow(userId: string) { - const credential = await this.findOneByUserId(userId); + const credential = await this.findOneByUserId(userId) if (!credential) { - throw new Error('TOTP credential not found'); + throw new Error('TOTP credential not found') } - return credential; + return credential } async create(userId: string) { - const twoFactorSecret = await new HMAC('SHA-1').generateKey(); + const twoFactorSecret = await new HMAC('SHA-1').generateKey() try { return await this.credentialsRepository.create({ user_id: userId, secret_data: encodeHex(twoFactorSecret), - type: 'totp' - }); + type: 'totp', + }) } catch (e) { - console.error(e); - return null; + console.error(e) + return null } } async deleteOneByUserId(userId: string) { - return this.credentialsRepository.deleteByUserId(userId); + return this.credentialsRepository.deleteByUserId(userId) } async deleteOneByUserIdAndType(userId: string, type: CredentialsType) { @@ -49,10 +45,10 @@ export class TotpService { } async verify(userId: string, code: string) { - const credential = await this.credentialsRepository.findTOTPCredentialsByUserId(userId); + const credential = await this.credentialsRepository.findTOTPCredentialsByUserId(userId) if (!credential) { - throw new Error('TOTP credential not found'); + throw new Error('TOTP credential not found') } return await new TOTPController().verify(code, decodeHex(credential.secret_data)) } -} \ No newline at end of file +} diff --git a/src/lib/server/api/services/users.service.ts b/src/lib/server/api/services/users.service.ts index 75ca372..89e6eea 100644 --- a/src/lib/server/api/services/users.service.ts +++ b/src/lib/server/api/services/users.service.ts @@ -1,12 +1,12 @@ -import { inject, injectable } from 'tsyringe'; -import {type UpdateUser, UsersRepository} from '../repositories/users.repository'; -import type {SignupUsernameEmailDto} from "$lib/dtos/signup-username-email.dto"; -import {TokensService} from "$lib/server/api/services/tokens.service"; -import {CredentialsRepository} from "$lib/server/api/repositories/credentials.repository"; -import {CredentialsType} from "$lib/server/api/infrastructure/database/tables"; -import {UserRolesService} from "$lib/server/api/services/user_roles.service"; -import { CollectionsService } from './collections.service'; -import { WishlistsService } from './wishlists.service'; +import type { SignupUsernameEmailDto } from '$lib/server/api/dtos/signup-username-email.dto' +import { CredentialsRepository } from '$lib/server/api/repositories/credentials.repository' +import { TokensService } from '$lib/server/api/services/tokens.service' +import { UserRolesService } from '$lib/server/api/services/user_roles.service' +import { inject, injectable } from 'tsyringe' +import { CredentialsType } from '../databases/tables' +import { type UpdateUser, UsersRepository } from '../repositories/users.repository' +import { CollectionsService } from './collections.service' +import { WishlistsService } from './wishlists.service' @injectable() export class UsersService { @@ -16,69 +16,69 @@ export class UsersService { @inject(TokensService) private readonly tokenService: TokensService, @inject(UsersRepository) private readonly usersRepository: UsersRepository, @inject(UserRolesService) private readonly userRolesService: UserRolesService, - @inject(WishlistsService) private readonly wishlistsService: WishlistsService - ) { } + @inject(WishlistsService) private readonly wishlistsService: WishlistsService, + ) {} async create(data: SignupUsernameEmailDto) { - const { firstName, lastName, email, username, password } = data; + const { firstName, lastName, email, username, password } = data - const hashedPassword = await this.tokenService.createHashedToken(password); + const hashedPassword = await this.tokenService.createHashedToken(password) const user = await this.usersRepository.create({ first_name: firstName, last_name: lastName, email, username, - }); + }) if (!user) { - return null; + return null } const credentials = await this.credentialsRepository.create({ user_id: user.id, type: CredentialsType.PASSWORD, secret_data: hashedPassword, - }); + }) if (!credentials) { - await this.usersRepository.delete(user.id); - return null; + await this.usersRepository.delete(user.id) + return null } - await this.userRolesService.addRoleToUser(user.id, 'user', true); + await this.userRolesService.addRoleToUser(user.id, 'user', true) - await this.wishlistsService.createEmptyNoName(user.id); - await this.collectionsService.createEmptyNoName(user.id); + await this.wishlistsService.createEmptyNoName(user.id) + await this.collectionsService.createEmptyNoName(user.id) - return user; + return user } async updateUser(userId: string, data: UpdateUser) { - return this.usersRepository.update(userId, data); + return this.usersRepository.update(userId, data) } async findOneByUsername(username: string) { - return this.usersRepository.findOneByUsername(username); + return this.usersRepository.findOneByUsername(username) } async findOneByEmail(email: string) { - return this.usersRepository.findOneByEmail(email); + return this.usersRepository.findOneByEmail(email) } async findOneById(id: string) { - return this.usersRepository.findOneById(id); + return this.usersRepository.findOneById(id) } async verifyPassword(userId: string, data: { password: string }) { - const user = await this.usersRepository.findOneById(userId); + const user = await this.usersRepository.findOneById(userId) if (!user) { - throw new Error('User not found'); + throw new Error('User not found') } - const credential = await this.credentialsRepository.findOneByUserIdAndType(userId, CredentialsType.PASSWORD); + const credential = await this.credentialsRepository.findOneByUserIdAndType(userId, CredentialsType.PASSWORD) if (!credential) { - throw new Error('Password credentials not found'); + throw new Error('Password credentials not found') } - const { password } = data; - return this.tokenService.verifyHashedToken(credential.secret_data, password); + const { password } = data + return this.tokenService.verifyHashedToken(credential.secret_data, password) } -} \ No newline at end of file +} diff --git a/src/lib/server/auth-utils.ts b/src/lib/server/auth-utils.ts index 949b0d2..d0d6693 100644 --- a/src/lib/server/auth-utils.ts +++ b/src/lib/server/auth-utils.ts @@ -1,19 +1,19 @@ -import { generateIdFromEntropySize, type Session, type User } from 'lucia'; -import { TimeSpan, createDate } from 'oslo'; -import { eq } from 'drizzle-orm'; -import { db } from './api/infrastructure/database/index'; -import { password_reset_tokens } from './api/infrastructure/database/tables'; +import { eq } from 'drizzle-orm' +import { type Session, type User, generateIdFromEntropySize } from 'lucia' +import { TimeSpan, createDate } from 'oslo' +import { password_reset_tokens } from './api/databases/tables' +import { db } from './api/packages/drizzle' export async function createPasswordResetToken(userId: string): Promise { // optionally invalidate all existing tokens - await db.delete(password_reset_tokens).where(eq(password_reset_tokens.user_id, userId)); - const tokenId = generateIdFromEntropySize(40); + await db.delete(password_reset_tokens).where(eq(password_reset_tokens.user_id, userId)) + const tokenId = generateIdFromEntropySize(40) await db.insert(password_reset_tokens).values({ id: tokenId, user_id: userId, expires_at: createDate(new TimeSpan(2, 'h')), - }); - return tokenId; + }) + return tokenId } /** @@ -24,7 +24,7 @@ export async function createPasswordResetToken(userId: string): Promise * @returns True if the user is not fully authenticated, otherwise false. */ export function userNotFullyAuthenticated(user: User | null, session: Session | null) { - return user && session && session.isTwoFactorAuthEnabled && !session.isTwoFactorAuthenticated; + return user && session && session.isTwoFactorAuthEnabled && !session.isTwoFactorAuthenticated } /** @@ -35,7 +35,7 @@ export function userNotFullyAuthenticated(user: User | null, session: Session | * @returns {boolean} True if the user is not fully authenticated, otherwise false. */ export function userNotAuthenticated(user: User | null, session: Session | null) { - return !user || !session || userNotFullyAuthenticated(user, session); + return !user || !session || userNotFullyAuthenticated(user, session) } /** @@ -46,5 +46,5 @@ export function userNotAuthenticated(user: User | null, session: Session | null) * @returns {boolean} True if the user is fully authenticated, otherwise false. */ export function userFullyAuthenticated(user: User | null, session: Session | null) { - return !userNotAuthenticated(user, session); + return !userNotAuthenticated(user, session) } diff --git a/src/lib/utils/db/categoryUtils.ts b/src/lib/utils/db/categoryUtils.ts index c2317de..780685d 100644 --- a/src/lib/utils/db/categoryUtils.ts +++ b/src/lib/utils/db/categoryUtils.ts @@ -1,83 +1,77 @@ -import { error } from '@sveltejs/kit'; -import { eq } from 'drizzle-orm'; -import kebabCase from 'just-kebab-case'; -import { PUBLIC_SITE_URL } from '$env/static/public'; -import db from '../../../db'; -import { - externalIds, - type Mechanics, - type Categories, - categories, - categoriesToExternalIds, -} from '$db/schema'; +import { PUBLIC_SITE_URL } from '$env/static/public' +import { type Categories, type Mechanics, categoriesTable, categoriesToExternalIdsTable, externalIds } from '$lib/server/api/databases/tables' +import { db } from '$lib/server/api/packages/drizzle' +import { error } from '@sveltejs/kit' +import { eq } from 'drizzle-orm' +import kebabCase from 'just-kebab-case' export async function createCategory(locals: App.Locals, category: Categories, externalId: string) { if (!category || !externalId || externalId === '') { - error(400, 'Invalid Request'); + error(400, 'Invalid Request') } try { const dbExternalId = await db.query.externalIds.findFirst({ where: eq(externalIds.externalId, externalId), - }); + }) if (dbExternalId) { const foundCategory = await db .select({ - id: categories.id, - name: categories.name, - slug: categories.slug, + id: categoriesTable.id, + name: categoriesTable.name, + slug: categoriesTable.slug, }) - .from(categories) - .leftJoin(categoriesToExternalIds, eq(categoriesToExternalIds.externalId, externalId)); - console.log('Mechanic already exists', foundCategory); + .from(categoriesTable) + .leftJoin(categoriesToExternalIdsTable, eq(categoriesToExternalIdsTable.externalId, externalId)) + console.log('Mechanic already exists', foundCategory) if (foundCategory.length > 0) { - console.log('Mechanic name', foundCategory[0].name); + console.log('Mechanic name', foundCategory[0].name) return new Response('Mechanic already exists', { headers: { 'Content-Type': 'application/json', Location: `${PUBLIC_SITE_URL}/api/mechanic/${foundCategory[0].id}`, }, status: 409, - }); + }) } } - let dbCategory: Mechanics[] = []; - console.log('Creating category', JSON.stringify(category, null, 2)); + let dbCategory: Mechanics[] = [] + console.log('Creating category', JSON.stringify(category, null, 2)) await db.transaction(async (transaction) => { dbCategory = await transaction - .insert(categories) + .insert(categoriesTable) .values({ name: category.name, slug: kebabCase(category.name ?? category.slug ?? ''), }) - .returning(); + .returning() const dbExternalIds = await transaction .insert(externalIds) .values({ externalId, type: 'category', }) - .returning({ id: externalIds.id }); - await transaction.insert(categoriesToExternalIds).values({ + .returning({ id: externalIds.id }) + await transaction.insert(categoriesToExternalIdsTable).values({ categoryId: dbCategory[0].id, externalId: dbExternalIds[0].id, - }); - }); + }) + }) if (dbCategory.length === 0) { return new Response('Could not create category', { status: 500, - }); + }) } - console.log('Created category', JSON.stringify(dbCategory[0], null, 2)); + console.log('Created category', JSON.stringify(dbCategory[0], null, 2)) return new Response(JSON.stringify(dbCategory[0]), { status: 201, - }); + }) } catch (e) { - console.error(e); - throw new Error('Something went wrong creating Category'); + console.error(e) + throw new Error('Something went wrong creating Category') } } diff --git a/src/lib/utils/db/expansionUtils.ts b/src/lib/utils/db/expansionUtils.ts index a4c9cd1..d6ff8db 100644 --- a/src/lib/utils/db/expansionUtils.ts +++ b/src/lib/utils/db/expansionUtils.ts @@ -1,60 +1,57 @@ -import { error } from '@sveltejs/kit'; -import { and, eq } from 'drizzle-orm'; -import db from '../../../db'; -import { type Expansions, expansions } from '$db/schema'; -import { PUBLIC_SITE_URL } from '$env/static/public'; +import { PUBLIC_SITE_URL } from '$env/static/public' +import { type Expansions, expansions } from '$lib/server/api/databases/tables' +import { db } from '$lib/server/api/packages/drizzle' +import { error } from '@sveltejs/kit' +import { and, eq } from 'drizzle-orm' export async function createExpansion(locals: App.Locals, expansion: Expansions) { if (!expansion || expansion?.base_game_id === '' || expansion?.game_id === '') { - error(400, 'Invalid Request'); + error(400, 'Invalid Request') } try { const foundExpansion = await db.query.expansions.findFirst({ - where: and( - eq(expansions.base_game_id, expansion.base_game_id), - eq(expansions.game_id, expansion.game_id), - ), + where: and(eq(expansions.base_game_id, expansion.base_game_id), eq(expansions.game_id, expansion.game_id)), columns: { id: true, game_id: true, base_game_id: true, }, - }); - console.log('Expansion already exists', foundExpansion); + }) + console.log('Expansion already exists', foundExpansion) if (foundExpansion) { - console.log('Expansion Game ID', foundExpansion.game_id); + console.log('Expansion Game ID', foundExpansion.game_id) return new Response('Expansion already exists', { headers: { 'Content-Type': 'application/json', Location: `${PUBLIC_SITE_URL}/api/game/${foundExpansion.game_id}`, }, status: 409, - }); + }) } - console.log('Creating expansion', JSON.stringify(expansion, null, 2)); + console.log('Creating expansion', JSON.stringify(expansion, null, 2)) const dbExpansion = await db .insert(expansions) .values({ base_game_id: expansion.base_game_id, game_id: expansion.game_id, }) - .returning(); + .returning() if (dbExpansion.length === 0) { return new Response('Could not create expansion', { status: 500, - }); + }) } - console.log('Created expansion', JSON.stringify(dbExpansion[0], null, 2)); + console.log('Created expansion', JSON.stringify(dbExpansion[0], null, 2)) return new Response(JSON.stringify(dbExpansion[0]), { status: 201, - }); + }) } catch (e) { - console.error(e); - throw new Error('Something went wrong creating Expansion'); + console.error(e) + throw new Error('Something went wrong creating Expansion') } } diff --git a/src/lib/utils/db/gameUtils.ts b/src/lib/utils/db/gameUtils.ts index e96e4ed..b8a375b 100644 --- a/src/lib/utils/db/gameUtils.ts +++ b/src/lib/utils/db/gameUtils.ts @@ -1,36 +1,36 @@ -import kebabCase from 'just-kebab-case'; -import db from '../../../db'; -import { externalIds, gamesToExternalIds, type Games, games } from '$db/schema'; -import { eq } from 'drizzle-orm'; -import { error } from '@sveltejs/kit'; -import { PUBLIC_SITE_URL } from '$env/static/public'; +import { PUBLIC_SITE_URL } from '$env/static/public' +import { type Games, externalIds, games, gamesToExternalIds } from '$lib/server/api/databases/tables' +import { db } from '$lib/server/api/packages/drizzle' +import { error } from '@sveltejs/kit' +import { eq } from 'drizzle-orm' +import kebabCase from 'just-kebab-case' export async function getGame(locals: App.Locals, id: string) { if (!id || id === '') { - error(400, 'Invalid Request'); + error(400, 'Invalid Request') } try { return await db.query.games.findFirst({ where: eq(games.id, id), - }); + }) } catch (e) { - console.error(e); + console.error(e) return new Response('Could not get games', { status: 500, - }); + }) } } export async function createGame(locals: App.Locals, game: Games, externalId: string) { if (!game || !externalId || externalId === '') { - error(400, 'Invalid Request'); + error(400, 'Invalid Request') } try { const dbExternalId = await db.query.externalIds.findFirst({ where: eq(externalIds.externalId, externalId), - }); + }) if (dbExternalId) { const foundGame = await db @@ -40,22 +40,22 @@ export async function createGame(locals: App.Locals, game: Games, externalId: st slug: games.slug, }) .from(games) - .leftJoin(gamesToExternalIds, eq(gamesToExternalIds.externalId, externalId)); - console.log('Game already exists', foundGame); + .leftJoin(gamesToExternalIds, eq(gamesToExternalIds.externalId, externalId)) + console.log('Game already exists', foundGame) if (foundGame.length > 0) { - console.log('Game name', foundGame[0].name); + console.log('Game name', foundGame[0].name) return new Response('Game already exists', { headers: { 'Content-Type': 'application/json', Location: `${PUBLIC_SITE_URL}/api/game/${foundGame[0].id}`, }, status: 409, - }); + }) } } - let dbGames: Games[] = []; - console.log('Creating game', JSON.stringify(game, null, 2)); + let dbGames: Games[] = [] + console.log('Creating game', JSON.stringify(game, null, 2)) await db.transaction(async (transaction) => { dbGames = await transaction .insert(games) @@ -73,50 +73,46 @@ export async function createGame(locals: App.Locals, game: Games, externalId: st min_playtime: game.min_playtime, max_playtime: game.max_playtime, }) - .returning(); + .returning() const dbExternalIds = await transaction .insert(externalIds) .values({ externalId, type: 'game', }) - .returning({ id: externalIds.id }); + .returning({ id: externalIds.id }) await transaction.insert(gamesToExternalIds).values({ gameId: dbGames[0].id, externalId: dbExternalIds[0].id, - }); - }); + }) + }) if (dbGames.length === 0) { return new Response('Could not create game', { status: 500, - }); + }) } - console.log('Created game', JSON.stringify(dbGames[0], null, 2)); + console.log('Created game', JSON.stringify(dbGames[0], null, 2)) return new Response(JSON.stringify(dbGames[0]), { status: 201, - }); + }) } catch (e) { - console.error(e); - throw new Error('Something went wrong creating Game'); + console.error(e) + throw new Error('Something went wrong creating Game') } } -export async function createOrUpdateGameMinimal( - locals: App.Locals, - game: Games, - externalId: string, -) { +export async function createOrUpdateGameMinimal(locals: App.Locals, game: Games, externalId: string) { if (!game || !externalId || externalId === '') { - error(400, 'Invalid Request'); + error(400, 'Invalid Request') } - console.log('Creating or updating minimal game data', JSON.stringify(game, null, 2)); - const externalUrl = `https://boardgamegeek.com/boardgame/${externalId}`; + console.log('Creating or updating minimal game data', JSON.stringify(game, null, 2)) + const externalUrl = `https://boardgamegeek.com/boardgame/${externalId}` try { - let dbGames: Games[] = []; - console.log('Creating game', JSON.stringify(game, null, 2)); + let dbGames: Games[] = [] + console.log('Creating game', JSON.stringify(game, null, 2)) await db.transaction(async (transaction) => { dbGames = await transaction .insert(games) @@ -151,7 +147,7 @@ export async function createOrUpdateGameMinimal( max_playtime: game.max_playtime, }, }) - .returning(); + .returning() const dbExternalIds = await transaction .insert(externalIds) .values({ @@ -159,42 +155,42 @@ export async function createOrUpdateGameMinimal( type: 'game', }) .onConflictDoNothing() - .returning({ id: externalIds.id }); + .returning({ id: externalIds.id }) await transaction .insert(gamesToExternalIds) .values({ gameId: dbGames[0].id, externalId: dbExternalIds[0].id, }) - .onConflictDoNothing(); - }); + .onConflictDoNothing() + }) if (dbGames.length === 0) { return new Response('Could not create game', { status: 500, - }); + }) } - console.log('Created game', JSON.stringify(dbGames[0], null, 2)); + console.log('Created game', JSON.stringify(dbGames[0], null, 2)) return new Response(JSON.stringify(dbGames[0]), { status: 201, - }); + }) } catch (e) { - console.error(e); - throw new Error('Something went wrong creating Game'); + console.error(e) + throw new Error('Something went wrong creating Game') } } export async function createOrUpdateGame(locals: App.Locals, game: Games, externalId: string) { if (!game || !externalId || externalId === '') { - error(400, 'Invalid Request'); + error(400, 'Invalid Request') } try { - const externalUrl = `https://boardgamegeek.com/boardgame/${externalId}`; + const externalUrl = `https://boardgamegeek.com/boardgame/${externalId}` const dbExternalId = await db.query.externalIds.findFirst({ where: eq(externalIds.externalId, externalId), - }); + }) if (dbExternalId) { const foundGame = await db @@ -204,22 +200,22 @@ export async function createOrUpdateGame(locals: App.Locals, game: Games, extern slug: games.slug, }) .from(games) - .leftJoin(gamesToExternalIds, eq(gamesToExternalIds.externalId, externalId)); - console.log('Game already exists', foundGame); + .leftJoin(gamesToExternalIds, eq(gamesToExternalIds.externalId, externalId)) + console.log('Game already exists', foundGame) if (foundGame.length > 0) { - console.log('Game name', foundGame[0].name); + console.log('Game name', foundGame[0].name) return new Response('Game already exists', { headers: { 'Content-Type': 'application/json', Location: `${PUBLIC_SITE_URL}/api/game/${foundGame[0].id}`, }, status: 409, - }); + }) } } - let dbGames: Games[] = []; - console.log('Creating game', JSON.stringify(game, null, 2)); + let dbGames: Games[] = [] + console.log('Creating game', JSON.stringify(game, null, 2)) await db.transaction(async (transaction) => { dbGames = await transaction .insert(games) @@ -254,7 +250,7 @@ export async function createOrUpdateGame(locals: App.Locals, game: Games, extern max_playtime: game.max_playtime, }, }) - .returning(); + .returning() const dbExternalIds = await transaction .insert(externalIds) .values({ @@ -262,35 +258,35 @@ export async function createOrUpdateGame(locals: App.Locals, game: Games, extern type: 'game', }) .onConflictDoNothing() - .returning({ id: externalIds.id }); + .returning({ id: externalIds.id }) await transaction .insert(gamesToExternalIds) .values({ gameId: dbGames[0].id, externalId: dbExternalIds[0].id, }) - .onConflictDoNothing(); - }); + .onConflictDoNothing() + }) if (dbGames.length === 0) { return new Response('Could not create game', { status: 500, - }); + }) } - console.log('Created game', JSON.stringify(dbGames[0], null, 2)); + console.log('Created game', JSON.stringify(dbGames[0], null, 2)) return new Response(JSON.stringify(dbGames[0]), { status: 201, - }); + }) } catch (e) { - console.error(e); - throw new Error('Something went wrong creating Game'); + console.error(e) + throw new Error('Something went wrong creating Game') } } export async function updateGame(locals: App.Locals, game: Games, id: string) { if (!game || !id || id === '') { - error(400, 'Invalid Request'); + error(400, 'Invalid Request') } try { @@ -311,17 +307,17 @@ export async function updateGame(locals: App.Locals, game: Games, id: string) { max_playtime: game.max_playtime, }) .where(eq(games.id, id)) - .returning(); + .returning() return new Response(JSON.stringify(dbGame[0]), { headers: { 'Content-Type': 'application/json', }, - }); + }) } catch (e) { - console.error(e); + console.error(e) return new Response('Could not get publishers', { status: 500, - }); + }) } } diff --git a/src/lib/utils/db/mechanicUtils.ts b/src/lib/utils/db/mechanicUtils.ts index 393db1a..4d963c0 100644 --- a/src/lib/utils/db/mechanicUtils.ts +++ b/src/lib/utils/db/mechanicUtils.ts @@ -1,19 +1,19 @@ -import kebabCase from 'just-kebab-case'; -import db from '../../../db'; -import { externalIds, mechanics, mechanicsToExternalIds, type Mechanics } from '$db/schema'; -import { eq } from 'drizzle-orm'; -import { error } from '@sveltejs/kit'; -import { PUBLIC_SITE_URL } from '$env/static/public'; +import { PUBLIC_SITE_URL } from '$env/static/public' +import { type Mechanics, externalIds, mechanics, mechanicsToExternalIds } from '$lib/server/api/databases/tables' +import { db } from '$lib/server/api/packages/drizzle' +import { error } from '@sveltejs/kit' +import { eq } from 'drizzle-orm' +import kebabCase from 'just-kebab-case' export async function createMechanic(locals: App.Locals, mechanic: Mechanics, externalId: string) { if (!mechanic || !externalId || externalId === '') { - error(400, 'Invalid Request'); + error(400, 'Invalid Request') } try { const dbExternalId = await db.query.externalIds.findFirst({ where: eq(externalIds.externalId, externalId), - }); + }) if (dbExternalId) { const foundMechanic = await db @@ -23,22 +23,22 @@ export async function createMechanic(locals: App.Locals, mechanic: Mechanics, ex slug: mechanics.slug, }) .from(mechanics) - .leftJoin(mechanicsToExternalIds, eq(mechanicsToExternalIds.externalId, externalId)); - console.log('Mechanic already exists', foundMechanic); + .leftJoin(mechanicsToExternalIds, eq(mechanicsToExternalIds.externalId, externalId)) + console.log('Mechanic already exists', foundMechanic) if (foundMechanic.length > 0) { - console.log('Mechanic name', foundMechanic[0].name); + console.log('Mechanic name', foundMechanic[0].name) return new Response('Mechanic already exists', { headers: { 'Content-Type': 'application/json', Location: `${PUBLIC_SITE_URL}/api/mechanic/${foundMechanic[0].id}`, }, status: 409, - }); + }) } } - let dbMechanics: Mechanics[] = []; - console.log('Creating mechanic', JSON.stringify(mechanic, null, 2)); + let dbMechanics: Mechanics[] = [] + console.log('Creating mechanic', JSON.stringify(mechanic, null, 2)) await db.transaction(async (transaction) => { dbMechanics = await transaction .insert(mechanics) @@ -46,32 +46,32 @@ export async function createMechanic(locals: App.Locals, mechanic: Mechanics, ex name: mechanic.name, slug: kebabCase(mechanic.name || mechanic.slug || ''), }) - .returning(); + .returning() const dbExternalIds = await transaction .insert(externalIds) .values({ externalId, type: 'mechanic', }) - .returning({ id: externalIds.id }); + .returning({ id: externalIds.id }) await transaction.insert(mechanicsToExternalIds).values({ mechanicId: dbMechanics[0].id, externalId: dbExternalIds[0].id, - }); - }); + }) + }) if (dbMechanics.length === 0) { return new Response('Could not create mechanic', { status: 500, - }); + }) } - console.log('Created mechanic', JSON.stringify(dbMechanics[0], null, 2)); + console.log('Created mechanic', JSON.stringify(dbMechanics[0], null, 2)) return new Response(JSON.stringify(dbMechanics[0]), { status: 201, - }); + }) } catch (e) { - console.error(e); - throw new Error('Something went wrong creating Mechanic'); + console.error(e) + throw new Error('Something went wrong creating Mechanic') } } diff --git a/src/lib/utils/db/publisherUtils.ts b/src/lib/utils/db/publisherUtils.ts index edef0ef..a66fe4e 100644 --- a/src/lib/utils/db/publisherUtils.ts +++ b/src/lib/utils/db/publisherUtils.ts @@ -1,25 +1,25 @@ -import { error } from '@sveltejs/kit'; -import { eq } from 'drizzle-orm'; -import kebabCase from 'just-kebab-case'; -import db from '../../../db'; -import { externalIds, publishersToExternalIds, type Publishers, publishers } from '$db/schema'; -import { PUBLIC_SITE_URL } from '$env/static/public'; +import { PUBLIC_SITE_URL } from '$env/static/public' +import { type Publishers, externalIds, publishers, publishersToExternalIds } from '$lib/server/api/databases/tables' +import { db } from '$lib/server/api/packages/drizzle' +import { error } from '@sveltejs/kit' +import { eq } from 'drizzle-orm' +import kebabCase from 'just-kebab-case' export async function getPublisher(locals: App.Locals, id: string) { - const publisher = await db.select().from(publishers).where(eq(publishers.id, id)); + const publisher = await db.select().from(publishers).where(eq(publishers.id, id)) if (publisher.length === 0) { - error(404, 'not found'); + error(404, 'not found') } return new Response(JSON.stringify(publisher[0]), { headers: { 'Content-Type': 'application/json', }, - }); + }) } export async function updatePublisher(locals: App.Locals, publisher: Publishers, id: string) { if (!publisher || publisher.name === '' || !id || id === '') { - error(400, 'Invalid Request'); + error(400, 'Invalid Request') } try { @@ -30,33 +30,29 @@ export async function updatePublisher(locals: App.Locals, publisher: Publishers, slug: kebabCase(publisher.name || ''), }) .where(eq(publishers.id, id)) - .returning(); + .returning() return new Response(JSON.stringify(dbPublisher[0]), { headers: { 'Content-Type': 'application/json', }, - }); + }) } catch (e) { - console.error(e); + console.error(e) return new Response('Could not get publishers', { status: 500, - }); + }) } } -export async function createPublisher( - locals: App.Locals, - publisher: Publishers, - externalId: string, -) { +export async function createPublisher(locals: App.Locals, publisher: Publishers, externalId: string) { if (!publisher || !externalId || externalId === '') { - error(400, 'Invalid Request'); + error(400, 'Invalid Request') } try { const dbExternalId = await db.query.externalIds.findFirst({ where: eq(externalIds.externalId, externalId), - }); + }) if (dbExternalId) { const foundPublisher = await db @@ -66,22 +62,22 @@ export async function createPublisher( slug: publishers.slug, }) .from(publishers) - .leftJoin(publishersToExternalIds, eq(publishersToExternalIds.externalId, externalId)); - console.log('Publisher already exists', foundPublisher); + .leftJoin(publishersToExternalIds, eq(publishersToExternalIds.externalId, externalId)) + console.log('Publisher already exists', foundPublisher) if (foundPublisher.length > 0) { - console.log('Publisher name', foundPublisher[0].name); + console.log('Publisher name', foundPublisher[0].name) return new Response('Publisher already exists', { headers: { 'Content-Type': 'application/json', Location: `${PUBLIC_SITE_URL}/api/publisher/${foundPublisher[0].id}`, }, status: 409, - }); + }) } } - let dbPublishers: Publishers[] = []; - console.log('Creating publisher', JSON.stringify(publisher, null, 2)); + let dbPublishers: Publishers[] = [] + console.log('Creating publisher', JSON.stringify(publisher, null, 2)) await db.transaction(async (transaction) => { dbPublishers = await transaction .insert(publishers) @@ -89,32 +85,32 @@ export async function createPublisher( name: publisher.name, slug: kebabCase(publisher.name || publisher.slug || ''), }) - .returning(); + .returning() const dbExternalIds = await transaction .insert(externalIds) .values({ externalId, type: 'publisher', }) - .returning({ id: externalIds.id }); + .returning({ id: externalIds.id }) await transaction.insert(publishersToExternalIds).values({ publisherId: dbPublishers[0].id, externalId: dbExternalIds[0].id, - }); - }); + }) + }) if (dbPublishers.length === 0) { return new Response('Could not create publisher', { status: 500, - }); + }) } - console.log('Created publisher', JSON.stringify(dbPublishers[0], null, 2)); + console.log('Created publisher', JSON.stringify(dbPublishers[0], null, 2)) return new Response(JSON.stringify(dbPublishers[0]), { status: 201, - }); + }) } catch (e) { - console.error(e); - throw new Error('Something went wrong creating Publisher'); + console.error(e) + throw new Error('Something went wrong creating Publisher') } } diff --git a/src/routes/(app)/(protected)/admin/+layout.server.ts b/src/routes/(app)/(protected)/admin/+layout.server.ts index 5cb8b59..9d42e4e 100644 --- a/src/routes/(app)/(protected)/admin/+layout.server.ts +++ b/src/routes/(app)/(protected)/admin/+layout.server.ts @@ -1,19 +1,19 @@ -import { redirect, loadFlash } from 'sveltekit-flash-message/server'; -import { forbiddenMessage, notSignedInMessage } from '$lib/flashMessages'; -import { eq } from 'drizzle-orm'; -import db from '../../../../db'; -import { userRoles } from '$db/schema'; -import { userNotAuthenticated } from '$lib/server/auth-utils'; +import { forbiddenMessage, notSignedInMessage } from '$lib/flashMessages' +import { user_roles } from '$lib/server/api/databases/tables' +import { db } from '$lib/server/api/packages/drizzle' +import { eq } from 'drizzle-orm' +import { loadFlash, redirect } from 'sveltekit-flash-message/server' export const load = loadFlash(async (event) => { - const { locals } = event; - const { user, session } = locals; - if (userNotAuthenticated(user, session)) { - redirect(302, '/login', notSignedInMessage, event); + const { locals } = event + + const authedUser = await locals.getAuthedUser() + if (!authedUser) { + throw redirect(302, '/login', notSignedInMessage, event) } - const dbUserRoles = await db.query.userRoles.findMany({ - where: eq(userRoles.user_id, user!.id!), + const dbUserRoles = await db.query.user_roles.findMany({ + where: eq(user_roles.user_id, authedUser.id), with: { role: { columns: { @@ -21,13 +21,13 @@ export const load = loadFlash(async (event) => { }, }, }, - }); + }) - const containsAdminRole = dbUserRoles.some((userRole) => userRole?.role?.name === 'admin'); + const containsAdminRole = dbUserRoles.some((userRole) => userRole?.role?.name === 'admin') if (!dbUserRoles?.length || !containsAdminRole) { - console.log('Not an admin'); - redirect(302, '/', forbiddenMessage, event); + console.log('Not an admin') + redirect(302, '/', forbiddenMessage, event) } - return {}; -}); + return {} +}) diff --git a/src/routes/(app)/(protected)/admin/users/+page.server.ts b/src/routes/(app)/(protected)/admin/users/+page.server.ts index b6e7d5a..065f749 100644 --- a/src/routes/(app)/(protected)/admin/users/+page.server.ts +++ b/src/routes/(app)/(protected)/admin/users/+page.server.ts @@ -1,22 +1,21 @@ -import { redirect } from 'sveltekit-flash-message/server'; -import type { PageServerLoad } from './$types'; -import db from '../../../../../db'; -import { userNotAuthenticated } from '$lib/server/auth-utils'; -import { notSignedInMessage } from '$lib/flashMessages'; +import { notSignedInMessage } from '$lib/flashMessages' +import { redirect } from 'sveltekit-flash-message/server' +import type { PageServerLoad } from './$types' export const load: PageServerLoad = async (event) => { - const { locals } = event; - const { user, session } = locals; - if (userNotAuthenticated(user, session)) { - redirect(302, '/login', notSignedInMessage, event); + const { locals } = event + + const authedUser = await locals.getAuthedUser() + if (!authedUser) { + throw redirect(302, '/login', notSignedInMessage, event) } - const users = await db.query.users.findMany({ - limit: 10, - offset: 0, - }); + // const users = await db.query.users.findMany({ + // limit: 10, + // offset: 0, + // }); return { - users, - }; -}; + // users, + } +} diff --git a/src/routes/(app)/(protected)/admin/users/[id]/+page.server.ts b/src/routes/(app)/(protected)/admin/users/[id]/+page.server.ts index 5d18bf0..85bdbd7 100644 --- a/src/routes/(app)/(protected)/admin/users/[id]/+page.server.ts +++ b/src/routes/(app)/(protected)/admin/users/[id]/+page.server.ts @@ -1,17 +1,17 @@ -import { and, eq, inArray, not } from 'drizzle-orm'; -import { redirect } from 'sveltekit-flash-message/server'; -import type { PageServerLoad } from './$types'; -import { forbiddenMessage, notSignedInMessage } from '$lib/flashMessages'; -import db from '../../../../../../db'; -import { roles, userRoles, usersTable } from '$db/schema'; +import { forbiddenMessage, notSignedInMessage } from '$lib/flashMessages' +import { roles, user_roles, usersTable } from '$lib/server/api/databases/tables' +import { db } from '$lib/server/api/packages/drizzle' +import { and, eq, inArray, not } from 'drizzle-orm' +import { redirect } from 'sveltekit-flash-message/server' +import type { PageServerLoad } from './$types' export const load: PageServerLoad = async (event) => { - const { params } = event; - const { id } = params; + const { params, locals } = event + const { id } = params - // TODO: Ensure admin user - if (!event.locals.user) { - redirect(302, '/login', notSignedInMessage, event); + const authedUser = await locals.getAuthedUser() + if (!authedUser) { + throw redirect(302, '/login', notSignedInMessage, event) } const foundUser = await db.query.usersTable.findFirst({ @@ -28,18 +28,16 @@ export const load: PageServerLoad = async (event) => { }, }, }, - }); + }) - const containsAdminRole = foundUser?.user_roles?.some( - (user_role) => user_role?.role?.name === 'admin', - ); + const containsAdminRole = foundUser?.user_roles?.some((user_role) => user_role?.role?.name === 'admin') if (!containsAdminRole) { - console.log('Not an admin'); - redirect(302, '/login', notSignedInMessage, event); + console.log('Not an admin') + redirect(302, '/login', notSignedInMessage, event) } - const currentRoleIds = foundUser?.user_roles?.map((user_role) => user_role?.role.cuid) || []; - let availableRoles: { name: string; cuid: string }[] = []; + const currentRoleIds = foundUser?.user_roles?.map((user_role) => user_role?.role.cuid) || [] + let availableRoles: { name: string; cuid: string }[] = [] if (currentRoleIds?.length > 0) { availableRoles = await db.query.roles.findMany({ where: not(inArray(roles.cuid, currentRoleIds)), @@ -47,26 +45,26 @@ export const load: PageServerLoad = async (event) => { name: true, cuid: true, }, - }); + }) } return { user: foundUser, availableRoles, - }; -}; + } +} export const actions = { addRole: async (event) => { - const { request, locals } = event; - const { user } = locals; + const { request, locals } = event + const { user } = locals if (!user) { - redirect(302, '/login', notSignedInMessage, event); + redirect(302, '/login', notSignedInMessage, event) } - const userRolesList = await db.query.userRoles.findMany({ - where: eq(userRoles.user_id, user.id), + const userRolesList = await db.query.user_roles.findMany({ + where: eq(user_roles.user_id, user.id), with: { role: { columns: { @@ -75,41 +73,41 @@ export const actions = { }, }, }, - }); + }) - console.log('userRoles', userRolesList); + console.log('userRoles', userRolesList) - const containsAdminRole = userRolesList.some((user_role) => user_role?.role?.name === 'admin'); - console.log('containsAdminRole', containsAdminRole); + const containsAdminRole = userRolesList.some((user_role) => user_role?.role?.name === 'admin') + console.log('containsAdminRole', containsAdminRole) if (!containsAdminRole) { - redirect(302, '/', forbiddenMessage, event); + redirect(302, '/', forbiddenMessage, event) } - const data = await request.formData(); - const role = data.get('role'); + const data = await request.formData() + const role = data.get('role') const dbRole = await db.query.roles.findFirst({ where: eq(roles.cuid, role?.toString() ?? ''), - }); - console.log('dbRole', dbRole); + }) + console.log('dbRole', dbRole) if (dbRole) { - await db.insert(userRoles).values({ + await db.insert(user_roles).values({ user_id: user.id, role_id: dbRole.id, - }); - redirect({ type: 'success', message: `Successfully added role ${dbRole.name}!` }, event); + }) + redirect({ type: 'success', message: `Successfully added role ${dbRole.name}!` }, event) } else { - redirect({ type: 'error', message: `Failed to add role ${dbRole.name}!` }, event); + redirect({ type: 'error', message: `Failed to add role ${role?.toString()}!` }, event) } }, removeRole: async (event) => { - const { request, locals } = event; - const { user } = locals; + const { request, locals } = event + const { user } = locals if (!user) { - redirect(302, '/login', notSignedInMessage, event); + redirect(302, '/login', notSignedInMessage, event) } - const userRolesList = await db.query.userRoles.findMany({ - where: eq(userRoles.user_id, user.id), + const userRolesList = await db.query.user_roles.findMany({ + where: eq(user_roles.user_id, user.id), with: { role: { columns: { @@ -118,26 +116,24 @@ export const actions = { }, }, }, - }); + }) - const containsAdminRole = userRolesList.some((user_role) => user_role?.role?.name === 'admin'); + const containsAdminRole = userRolesList.some((user_role) => user_role?.role?.name === 'admin') if (!containsAdminRole) { - redirect(302, '/', forbiddenMessage, event); + redirect(302, '/', forbiddenMessage, event) } - const data = await request.formData(); - const role = data.get('role'); + const data = await request.formData() + const role = data.get('role') const dbRole = await db.query.roles.findFirst({ where: eq(roles.cuid, role?.toString() ?? ''), - }); - console.log('dbRole', dbRole); + }) + console.log('dbRole', dbRole) if (dbRole) { - await db - .delete(userRoles) - .where(and(eq(userRoles.user_id, user.id), eq(userRoles.role_id, dbRole.id))); - redirect({ type: 'success', message: `Successfully removed role ${dbRole.name}!` }, event); + await db.delete(user_roles).where(and(eq(user_roles.user_id, user.id), eq(user_roles.role_id, dbRole.id))) + redirect({ type: 'success', message: `Successfully removed role ${dbRole.name}!` }, event) } else { - redirect({ type: 'error', message: `Failed to remove role ${role?.toString()} !` }, event); + redirect({ type: 'error', message: `Failed to remove role ${role?.toString()} !` }, event) } }, -}; +} diff --git a/src/routes/(app)/(protected)/collections/+page.server.ts b/src/routes/(app)/(protected)/collections/+page.server.ts index 07e63b5..55bad0b 100644 --- a/src/routes/(app)/(protected)/collections/+page.server.ts +++ b/src/routes/(app)/(protected)/collections/+page.server.ts @@ -1,18 +1,18 @@ -import { type Actions, error, fail } from '@sveltejs/kit'; -import { and, eq } from 'drizzle-orm'; -import { superValidate } from 'sveltekit-superforms/server'; -import { zod } from 'sveltekit-superforms/adapters'; -import { redirect } from 'sveltekit-flash-message/server'; -import { modifyListGameSchema } from '$lib/validations/zod-schemas'; -import { db } from '$lib/server/api/infrastructure/database'; -import { collection_items, collections, games } from '$lib/server/api/infrastructure/database/tables'; -import { notSignedInMessage } from '$lib/flashMessages'; -import { userNotAuthenticated } from '$lib/server/auth-utils'; +import { notSignedInMessage } from '$lib/flashMessages' +import { db } from '$lib/server/api/packages/drizzle' +import { userNotAuthenticated } from '$lib/server/auth-utils' +import { modifyListGameSchema } from '$lib/validations/zod-schemas' +import { type Actions, error, fail } from '@sveltejs/kit' +import { and, eq } from 'drizzle-orm' +import { redirect } from 'sveltekit-flash-message/server' +import { zod } from 'sveltekit-superforms/adapters' +import { superValidate } from 'sveltekit-superforms/server' +import { collection_items, collections, games } from '../../../../lib/server/api/databases/tables' export async function load(event) { - const { user, session } = event.locals; + const { user, session } = event.locals if (userNotAuthenticated(user, session)) { - redirect(302, '/login', notSignedInMessage, event); + redirect(302, '/login', notSignedInMessage, event) } try { @@ -23,39 +23,39 @@ export async function load(event) { created_at: true, }, where: eq(collections.user_id, user!.id!), - }); - console.log('collections', userCollections); + }) + console.log('collections', userCollections) if (userCollections?.length === 0) { - console.log('Collection was not found'); - return fail(404, {}); + console.log('Collection was not found') + return fail(404, {}) } return { collections: userCollections, - }; + } } catch (e) { - console.error(e); + console.error(e) } return { collections: [], - }; + } } export const actions: Actions = { // Add game to a wishlist add: async (event) => { - const form = await superValidate(event, zod(modifyListGameSchema)); + const form = await superValidate(event, zod(modifyListGameSchema)) if (!event.locals.user) { - throw fail(401); + throw fail(401) } - const user = event.locals.user; + const user = event.locals.user const game = await db.query.games.findFirst({ where: eq(games.id, form.data.id), - }); + }) if (!game) { // game = await prisma.game.create({ @@ -63,91 +63,84 @@ export const actions: Actions = { // name: form.name // } // }); - console.log('game not found'); - redirect(302, '/404'); + console.log('game not found') + redirect(302, '/404') } try { const collection = await db.query.collections.findFirst({ where: eq(collections.user_id, user.id), - }); + }) if (!collection) { - console.log('Wishlist not found'); - return error(404, 'Wishlist not found'); + console.log('Wishlist not found') + return error(404, 'Wishlist not found') } await db.insert(collection_items).values({ game_id: game.id, collection_id: collection.id, times_played: 0, - }); + }) return { form, - }; + } } catch (e) { - console.error(e); - return error(500, 'Something went wrong'); + console.error(e) + return error(500, 'Something went wrong') } }, // Create new wishlist create: async ({ locals }) => { if (!locals.user) { - throw fail(401); + throw fail(401) } - return error(405, 'Method not allowed'); + return error(405, 'Method not allowed') }, // Delete a wishlist delete: async ({ locals }) => { if (!locals.user) { - throw fail(401); + throw fail(401) } - return error(405, 'Method not allowed'); + return error(405, 'Method not allowed') }, // Remove game from a wishlist remove: async (event) => { - const { locals } = event; - const form = await superValidate(event, zod(modifyListGameSchema)); + const { locals } = event + const form = await superValidate(event, zod(modifyListGameSchema)) if (!locals.user) { - throw fail(401); + throw fail(401) } const game = await db.query.games.findFirst({ where: eq(games.id, form.data.id), - }); + }) if (!game) { - console.log('game not found'); - redirect(302, '/404'); + console.log('game not found') + redirect(302, '/404') } try { const collection = await db.query.collections.findFirst({ where: eq(collections.user_id, locals.user.id), - }); + }) if (!collection) { - console.log('Collection not found'); - return error(404, 'Collection not found'); + console.log('Collection not found') + return error(404, 'Collection not found') } - await db - .delete(collection_items) - .where( - and( - eq(collection_items.collection_id, collection.id), - eq(collection_items.game_id, game.id), - ), - ); + await db.delete(collection_items).where(and(eq(collection_items.collection_id, collection.id), eq(collection_items.game_id, game.id))) return { form, - }; + } } catch (e) { - console.error(e); - return error(500, 'Something went wrong'); + console.error(e) + return error(500, 'Something went wrong') } }, -}; +} diff --git a/src/routes/(app)/(protected)/collections/[cuid]/+page.server.ts b/src/routes/(app)/(protected)/collections/[cuid]/+page.server.ts index c8e3e45..fff5ac2 100644 --- a/src/routes/(app)/(protected)/collections/[cuid]/+page.server.ts +++ b/src/routes/(app)/(protected)/collections/[cuid]/+page.server.ts @@ -1,48 +1,50 @@ -import { type Actions, error, fail } from '@sveltejs/kit'; -import { and, eq } from 'drizzle-orm'; -import { zod } from 'sveltekit-superforms/adapters'; -import { superValidate } from 'sveltekit-superforms/server'; -import { redirect } from 'sveltekit-flash-message/server'; -import { modifyListGameSchema } from '$lib/validations/zod-schemas'; -import { db } from '$lib/server/api/infrastructure/database'; -import { notSignedInMessage } from '$lib/flashMessages.js'; -import { collections, games, collection_items } from '$lib/server/api/infrastructure/database/tables'; -import { userNotAuthenticated } from '$lib/server/auth-utils'; +import { notSignedInMessage } from '$lib/flashMessages.js' +import { db } from '$lib/server/api/packages/drizzle' +import { userNotAuthenticated } from '$lib/server/auth-utils' +import { modifyListGameSchema } from '$lib/validations/zod-schemas' +import { type Actions, error, fail } from '@sveltejs/kit' +import { and, eq } from 'drizzle-orm' +import { redirect } from 'sveltekit-flash-message/server' +import { zod } from 'sveltekit-superforms/adapters' +import { superValidate } from 'sveltekit-superforms/server' +import { collection_items, collections, games } from '../../../../../lib/server/api/databases/tables' export async function load(event) { - const { params, locals } = event; - const { cuid } = params; + const { params, locals } = event + const { cuid } = params - const authedUser = await locals.getAuthedUser(); + const authedUser = await locals.getAuthedUser() if (!authedUser) { - throw redirect(302, '/login', notSignedInMessage, event); + throw redirect(302, '/login', notSignedInMessage, event) } try { - const { data, errors } = await locals.api.collections[':cuid'].$get({ - param: { cuid } - }).then(locals.parseApiResponse); + const { data, errors } = await locals.api.collections[':cuid'] + .$get({ + param: { cuid }, + }) + .then(locals.parseApiResponse) if (errors) { - return error(500, 'Failed to fetch collection'); + return error(500, 'Failed to fetch collection') } - const { collection } = data; + const { collection } = data if (!collection) { - redirect(302, '/404'); + redirect(302, '/404') } - console.log('collection', collection); + console.log('collection', collection) return { collection, - }; + } } catch (e) { - console.error(e); + console.error(e) } - redirect(302, '/404'); + redirect(302, '/404') // const searchParams = Object.fromEntries(url?.searchParams); // console.log('searchParams', searchParams); @@ -128,17 +130,17 @@ export async function load(event) { export const actions: Actions = { // Add game to a wishlist add: async (event) => { - const { locals } = event; - const { user, session } = locals; + const { locals } = event + const { user, session } = locals if (userNotAuthenticated(user, session)) { - return fail(401); + return fail(401) } - const form = await superValidate(event, zod(modifyListGameSchema)); + const form = await superValidate(event, zod(modifyListGameSchema)) const game = await db.query.games.findFirst({ where: eq(games.id, form.data.id), - }); + }) if (!game) { // game = await prisma.game.create({ @@ -146,95 +148,88 @@ export const actions: Actions = { // name: form.name // } // }); - console.log('game not found'); - redirect(302, '/404'); + console.log('game not found') + redirect(302, '/404') } try { const collection = await db.query.collections.findFirst({ where: eq(collections.user_id, user!.id!), - }); + }) if (!collection) { - console.log('Wishlist not found'); - return error(404, 'Wishlist not found'); + console.log('Wishlist not found') + return error(404, 'Wishlist not found') } await db.insert(collection_items).values({ game_id: game.id, collection_id: collection.id, times_played: 0, - }); + }) return { form, - }; + } } catch (e) { - console.error(e); - return error(500, 'Something went wrong'); + console.error(e) + return error(500, 'Something went wrong') } }, // Create new wishlist create: async (event) => { - const { locals } = event; - const { user, session } = locals; + const { locals } = event + const { user, session } = locals if (userNotAuthenticated(user, session)) { - return fail(401); + return fail(401) } - return error(405, 'Method not allowed'); + return error(405, 'Method not allowed') }, // Delete a wishlist delete: async (event) => { - const { locals } = event; - const { user, session } = locals; + const { locals } = event + const { user, session } = locals if (userNotAuthenticated(user, session)) { - return fail(401); + return fail(401) } - return error(405, 'Method not allowed'); + return error(405, 'Method not allowed') }, // Remove game from a wishlist remove: async (event) => { - const { locals } = event; - const { user, session } = locals; + const { locals } = event + const { user, session } = locals if (userNotAuthenticated(user, session)) { - return fail(401); + return fail(401) } - const form = await superValidate(event, zod(modifyListGameSchema)); + const form = await superValidate(event, zod(modifyListGameSchema)) const game = await db.query.games.findFirst({ where: eq(games.id, form.data.id), - }); + }) if (!game) { - console.log('game not found'); - redirect(302, '/404'); + console.log('game not found') + redirect(302, '/404') } try { const collection = await db.query.collections.findFirst({ where: eq(collections.user_id, user!.id!), - }); + }) if (!collection) { - console.log('Collection not found'); - return error(404, 'Collection not found'); + console.log('Collection not found') + return error(404, 'Collection not found') } - await db - .delete(collection_items) - .where( - and( - eq(collection_items.collection_id, collection.id), - eq(collection_items.game_id, game.id), - ), - ); + await db.delete(collection_items).where(and(eq(collection_items.collection_id, collection.id), eq(collection_items.game_id, game.id))) return { form, - }; + } } catch (e) { - console.error(e); - return error(500, 'Something went wrong'); + console.error(e) + return error(500, 'Something went wrong') } }, -}; +} diff --git a/src/routes/(app)/(protected)/list/+layout.server.ts b/src/routes/(app)/(protected)/list/+layout.server.ts index c9b2c17..30ca610 100644 --- a/src/routes/(app)/(protected)/list/+layout.server.ts +++ b/src/routes/(app)/(protected)/list/+layout.server.ts @@ -1,29 +1,29 @@ -import { redirect } from 'sveltekit-flash-message/server'; -import { eq } from 'drizzle-orm'; -import { db } from '$lib/server/api/infrastructure/database'; -import { wishlists } from '$lib/server/api/infrastructure/database/tables'; -import { notSignedInMessage } from '$lib/flashMessages'; +import { notSignedInMessage } from '$lib/flashMessages' +import { wishlists } from '$lib/server/api/databases/tables' +import { db } from '$lib/server/api/packages/drizzle' +import { eq } from 'drizzle-orm' +import { redirect } from 'sveltekit-flash-message/server' export async function load(event) { - const { locals } = event; + const { locals } = event - const authedUser = await locals.getAuthedUser(); + const authedUser = await locals.getAuthedUser() if (!authedUser) { - throw redirect(302, '/login', notSignedInMessage, event); + throw redirect(302, '/login', notSignedInMessage, event) } try { const dbWishlists = await db.query.wishlists.findMany({ where: eq(wishlists.user_id, authedUser.id), - }); + }) return { wishlists: dbWishlists, - }; + } } catch (e) { - console.error(e); + console.error(e) } return { wishlists: [], - }; + } } diff --git a/src/routes/(app)/(protected)/list/[id]/+page.server.ts b/src/routes/(app)/(protected)/list/[id]/+page.server.ts index ff59f4d..147492d 100644 --- a/src/routes/(app)/(protected)/list/[id]/+page.server.ts +++ b/src/routes/(app)/(protected)/list/[id]/+page.server.ts @@ -1,19 +1,20 @@ -import { type Actions, fail } from '@sveltejs/kit'; -import { eq } from 'drizzle-orm'; -import { zod } from 'sveltekit-superforms/adapters'; -import { superValidate } from 'sveltekit-superforms/server'; -import { redirect } from 'sveltekit-flash-message/server'; -import db from '../../../../../db'; -import { modifyListGameSchema } from '$lib/validations/zod-schemas'; -import { games, wishlist_items, wishlists } from '$db/schema'; -import { userNotAuthenticated } from '$lib/server/auth-utils'; -import { notSignedInMessage } from '$lib/flashMessages'; +import { notSignedInMessage } from '$lib/flashMessages' +import { games, wishlist_items, wishlists } from '$lib/server/api/databases/tables' +import { db } from '$lib/server/api/packages/drizzle' +import { userNotAuthenticated } from '$lib/server/auth-utils' +import { modifyListGameSchema } from '$lib/validations/zod-schemas' +import { type Actions, fail } from '@sveltejs/kit' +import { eq } from 'drizzle-orm' +import { redirect } from 'sveltekit-flash-message/server' +import { zod } from 'sveltekit-superforms/adapters' +import { superValidate } from 'sveltekit-superforms/server' export async function load(event) { - const { params, locals } = event; - const { user, session } = locals; - if (userNotAuthenticated(user, session)) { - redirect(302, '/login', notSignedInMessage, event); + const { params, locals } = event + + const authedUser = await locals.getAuthedUser() + if (!authedUser) { + throw redirect(302, '/login', notSignedInMessage, event) } try { @@ -30,97 +31,97 @@ export async function load(event) { .from(wishlists) .leftJoin(wishlist_items, eq(wishlists.id, wishlist_items.wishlist_id)) .leftJoin(games, eq(games.id, wishlist_items.game_id)) - .where(eq(wishlists.id, params.id)); + .where(eq(wishlists.id, params.id)) return { wishlist, - }; + } } catch (e) { - console.error(e); - return {}; + console.error(e) + return {} } } export const actions: Actions = { // Add game to a wishlist add: async (event) => { - const { params, locals } = event; - const { user, session } = locals; + const { params, locals } = event + const { user, session } = locals if (userNotAuthenticated(user, session)) { - return fail(401); + return fail(401) } - const form = await superValidate(event, zod(modifyListGameSchema)); + const form = await superValidate(event, zod(modifyListGameSchema)) if (!locals.user) { - throw fail(401); + throw fail(401) } if (!params?.id) { throw fail(400, { message: 'Invalid Request', - }); + }) } const game = await db.query.games.findFirst({ where: eq(games.id, form.id), - }); + }) if (!game) { return fail(400, { message: 'Game not found', - }); + }) } const wishlist = await db.query.wishlists.findFirst({ where: eq(wishlists.id, params.id), - }); + }) if (wishlist?.user_id !== locals.user.id) { return fail(401, { message: 'Unauthorized', - }); + }) } if (!wishlist) { - redirect(302, '/404'); + redirect(302, '/404') } const wishlistItem = await db.insert(wishlist_items).values({ game_id: game.id, wishlist_id: wishlist.id, - }); + }) if (!wishlistItem) { return fail(500, { message: 'Something went wrong', - }); + }) } return { form, - }; + } }, // Create new wishlist create: async (event) => { - const { locals } = event; - const { user, session } = locals; + const { locals } = event + const { user, session } = locals if (userNotAuthenticated(user, session)) { - return fail(401); + return fail(401) } }, // Delete a wishlist delete: async (event) => { - const { locals } = event; - const { user, session } = locals; + const { locals } = event + const { user, session } = locals if (userNotAuthenticated(user, session)) { - return fail(401); + return fail(401) } }, // Remove game from a wishlist remove: async (event) => { - const { locals } = event; - const { user, session } = locals; + const { locals } = event + const { user, session } = locals if (userNotAuthenticated(user, session)) { - return fail(401); + return fail(401) } }, -}; +} diff --git a/src/routes/(app)/(protected)/profile/+page.server.ts b/src/routes/(app)/(protected)/profile/+page.server.ts index 0c732cd..15e4e46 100644 --- a/src/routes/(app)/(protected)/profile/+page.server.ts +++ b/src/routes/(app)/(protected)/profile/+page.server.ts @@ -1,138 +1,129 @@ -import { fail, type Actions } from '@sveltejs/kit'; -import { z } from 'zod'; -import { eq } from 'drizzle-orm'; -import { zod } from 'sveltekit-superforms/adapters'; -import { message, setError, superValidate } from 'sveltekit-superforms/server'; -import { redirect } from 'sveltekit-flash-message/server'; -import { changeEmailSchema, profileSchema } from '$lib/validations/account'; -import { notSignedInMessage } from '$lib/flashMessages'; -import { db } from '$lib/server/api/infrastructure/database'; -import type { PageServerLoad } from './$types'; -import { usersTable, credentialsTable } from '$lib/server/api/infrastructure/database/tables'; -import { userNotAuthenticated } from '$lib/server/auth-utils'; -import { updateProfileDto } from "$lib/dtos/update-profile.dto"; -import { updateEmailDto } from "$lib/dtos/update-email.dto"; +import { updateEmailDto } from '$lib/dtos/update-email.dto' +import { updateProfileDto } from '$lib/dtos/update-profile.dto' +import { notSignedInMessage } from '$lib/flashMessages' +import { usersTable } from '$lib/server/api/databases/tables' +import { db } from '$lib/server/api/packages/drizzle' +import { changeEmailSchema, profileSchema } from '$lib/validations/account' +import { type Actions, fail } from '@sveltejs/kit' +import { eq } from 'drizzle-orm' +import { redirect } from 'sveltekit-flash-message/server' +import { zod } from 'sveltekit-superforms/adapters' +import { message, setError, superValidate } from 'sveltekit-superforms/server' +import { z } from 'zod' +import type { PageServerLoad } from './$types' export const load: PageServerLoad = async (event) => { - const { locals } = event; + const { locals } = event - const authedUser = await locals.getAuthedUser(); - if (!authedUser) { - throw redirect(302, '/login', notSignedInMessage, event); - } + const authedUser = await locals.getAuthedUser() + if (!authedUser) { + throw redirect(302, '/login', notSignedInMessage, event) + } - console.log('authedUser', authedUser); - // if (userNotAuthenticated(user, session)) { - // redirect(302, '/login', notSignedInMessage, event); - // } - // const dbUser = await db.query.usersTable.findFirst({ - // where: eq(usersTable.id, user!.id!), - // }); + console.log('authedUser', authedUser) + // if (userNotAuthenticated(user, session)) { + // redirect(302, '/login', notSignedInMessage, event); + // } + // const dbUser = await db.query.usersTable.findFirst({ + // where: eq(usersTable.id, user!.id!), + // }); - const profileForm = await superValidate(zod(profileSchema), { - defaults: { - firstName: authedUser?.firstName ?? '', - lastName: authedUser?.lastName ?? '', - username: authedUser?.username ?? '', - }, - }); - const emailForm = await superValidate(zod(changeEmailSchema), { - defaults: { - email: authedUser?.email ?? '', - }, - }); + const profileForm = await superValidate(zod(profileSchema), { + defaults: { + firstName: authedUser?.firstName ?? '', + lastName: authedUser?.lastName ?? '', + username: authedUser?.username ?? '', + }, + }) + const emailForm = await superValidate(zod(changeEmailSchema), { + defaults: { + email: authedUser?.email ?? '', + }, + }) - // const twoFactorDetails = await db.query.twoFactor.findFirst({ - // where: eq(twoFactor.userId, authedUser!.id!), - // }); + // const twoFactorDetails = await db.query.twoFactor.findFirst({ + // where: eq(twoFactor.userId, authedUser!.id!), + // }); - return { - profileForm, - emailForm, - hasSetupTwoFactor: false //!!twoFactorDetails?.enabled, - }; -}; + return { + profileForm, + emailForm, + hasSetupTwoFactor: false, //!!twoFactorDetails?.enabled, + } +} const changeEmailIfNotEmpty = z.object({ - email: z - .string() - .trim() - .max(64, { message: 'Email must be less than 64 characters' }) - .email({ message: 'Please enter a valid email' }), -}); + email: z.string().trim().max(64, { message: 'Email must be less than 64 characters' }).email({ message: 'Please enter a valid email' }), +}) export const actions: Actions = { - profileUpdate: async (event) => { - const { locals } = event; + profileUpdate: async (event) => { + const { locals } = event - const authedUser = await locals.getAuthedUser(); + const authedUser = await locals.getAuthedUser() - if (!authedUser) { - redirect(302, '/login', notSignedInMessage, event); - } + if (!authedUser) { + redirect(302, '/login', notSignedInMessage, event) + } - const form = await superValidate(event, zod(updateProfileDto)); + const form = await superValidate(event, zod(updateProfileDto)) - const { error } = await locals.api.me.update.profile.$put({ json: form.data }).then(locals.parseApiResponse); - console.log('data from profile update', error); - if (error) { - return setError(form, 'username', error); - } + const { error } = await locals.api.me.update.profile.$put({ json: form.data }).then(locals.parseApiResponse) + console.log('data from profile update', error) + if (error) { + return setError(form, 'username', error) + } - if (!form.valid) { - return fail(400, { - form, - }); - } + if (!form.valid) { + return fail(400, { + form, + }) + } - console.log('profile updated successfully'); - return message(form, { type: 'success', message: 'Profile updated successfully!' }); - }, - changeEmail: async (event) => { - const form = await superValidate(event, zod(updateEmailDto)); + console.log('profile updated successfully') + return message(form, { type: 'success', message: 'Profile updated successfully!' }) + }, + changeEmail: async (event) => { + const form = await superValidate(event, zod(updateEmailDto)) - const newEmail = form.data?.email; - if ( - !form.valid || - !newEmail || - (newEmail !== '' && !changeEmailIfNotEmpty.safeParse(form.data).success) - ) { - return fail(400, { - form, - }); - } + const newEmail = form.data?.email + if (!form.valid || !newEmail || (newEmail !== '' && !changeEmailIfNotEmpty.safeParse(form.data).success)) { + return fail(400, { + form, + }) + } - if (!event.locals.user) { - redirect(302, '/login', notSignedInMessage, event); - } + if (!event.locals.user) { + redirect(302, '/login', notSignedInMessage, event) + } - const user = event.locals.user; - const existingUser = await db.query.usersTable.findFirst({ - where: eq(usersTable.email, newEmail), - }); + const user = event.locals.user + const existingUser = await db.query.usersTable.findFirst({ + where: eq(usersTable.email, newEmail), + }) - if (existingUser && existingUser.id !== user.id) { - return setError(form, 'email', 'That email is already taken'); - } + if (existingUser && existingUser.id !== user.id) { + return setError(form, 'email', 'That email is already taken') + } - await db.update(usersTable).set({ email: form.data.email }).where(eq(usersTable.id, user.id)); + await db.update(usersTable).set({ email: form.data.email }).where(eq(usersTable.id, user.id)) - // if (user.email !== form.data.email) { - // Send email to confirm new email? - // auth.update - // await locals.prisma.key.update({ - // where: { - // id: 'emailpassword:' + user.email - // }, - // data: { - // id: 'emailpassword:' + form.data.email - // } - // }); - // auth.updateUserAttributes(user.user_id, { - // receiveEmail: false - // }); - // } + // if (user.email !== form.data.email) { + // Send email to confirm new email? + // auth.update + // await locals.prisma.key.update({ + // where: { + // id: 'emailpassword:' + user.email + // }, + // data: { + // id: 'emailpassword:' + form.data.email + // } + // }); + // auth.updateUserAttributes(user.user_id, { + // receiveEmail: false + // }); + // } - return message(form, { type: 'success', message: 'Email updated successfully!' }); - }, -}; + return message(form, { type: 'success', message: 'Email updated successfully!' }) + }, +} diff --git a/src/routes/(app)/(protected)/profile/+page.svelte b/src/routes/(app)/(protected)/profile/+page.svelte index c6bc370..7434d86 100644 --- a/src/routes/(app)/(protected)/profile/+page.svelte +++ b/src/routes/(app)/(protected)/profile/+page.svelte @@ -1,42 +1,49 @@
diff --git a/src/routes/(app)/(protected)/profile/security/mfa/+page.server.ts b/src/routes/(app)/(protected)/profile/security/mfa/+page.server.ts index cbb2dc9..59cf933 100644 --- a/src/routes/(app)/(protected)/profile/security/mfa/+page.server.ts +++ b/src/routes/(app)/(protected)/profile/security/mfa/+page.server.ts @@ -1,22 +1,22 @@ -import { type Actions, fail, error } from '@sveltejs/kit' +import { StatusCodes } from '$lib/constants/status-codes' +import { notSignedInMessage } from '$lib/flashMessages' +import { db } from '$lib/server/api/packages/drizzle' +import { userNotAuthenticated } from '$lib/server/auth-utils' +import { addTwoFactorSchema, removeTwoFactorSchema } from '$lib/validations/account' +import env from '$src/env' +import { type Actions, error, fail } from '@sveltejs/kit' import { eq } from 'drizzle-orm' -import { encodeHex, decodeHex } from 'oslo/encoding' -import { Argon2id } from 'oslo/password' -import { createTOTPKeyURI, TOTPController } from 'oslo/otp' -import { HMAC } from 'oslo/crypto' import kebabCase from 'just-kebab-case' +import { HMAC } from 'oslo/crypto' +import { decodeHex, encodeHex } from 'oslo/encoding' +import { TOTPController, createTOTPKeyURI } from 'oslo/otp' +import { Argon2id } from 'oslo/password' import QRCode from 'qrcode' +import { redirect, setFlash } from 'sveltekit-flash-message/server' import { zod } from 'sveltekit-superforms/adapters' import { setError, superValidate } from 'sveltekit-superforms/server' -import { redirect, setFlash } from 'sveltekit-flash-message/server' import type { PageServerLoad } from '../../$types' -import { addTwoFactorSchema, removeTwoFactorSchema } from '$lib/validations/account' -import { notSignedInMessage } from '$lib/flashMessages' -import { db } from '$lib/server/api/infrastructure/database' -import { recoveryCodesTable, credentialsTable, usersTable, type Credentials } from '$lib/server/api/infrastructure/database/tables' -import { userNotAuthenticated } from '$lib/server/auth-utils' -import env from '$src/env' -import { StatusCodes } from '$lib/constants/status-codes' +import { type Credentials, credentialsTable, recoveryCodesTable, usersTable } from '../../../../../../lib/server/api/databases/tables' export const load: PageServerLoad = async (event) => { const { locals } = event @@ -102,9 +102,11 @@ export const actions: Actions = { }) } - const { error: verifyPasswordError } = await locals.api.me.verify.password.$post({ - json: { password: addTwoFactorForm.data.current_password }, - }).then(locals.parseApiResponse) + const { error: verifyPasswordError } = await locals.api.me.verify.password + .$post({ + json: { password: addTwoFactorForm.data.current_password }, + }) + .then(locals.parseApiResponse) if (verifyPasswordError) { console.log(verifyPasswordError) diff --git a/src/routes/(app)/(protected)/profile/security/mfa/recovery-codes/+page.server.ts b/src/routes/(app)/(protected)/profile/security/mfa/recovery-codes/+page.server.ts index 84ceece..663f788 100644 --- a/src/routes/(app)/(protected)/profile/security/mfa/recovery-codes/+page.server.ts +++ b/src/routes/(app)/(protected)/profile/security/mfa/recovery-codes/+page.server.ts @@ -1,18 +1,18 @@ -import { eq } from 'drizzle-orm'; -import { Argon2id } from 'oslo/password'; -import { alphabet, generateRandomString } from 'oslo/crypto'; -import { redirect } from 'sveltekit-flash-message/server'; -import { db } from '$lib/server/api/infrastructure/database'; -import { notSignedInMessage } from '$lib/flashMessages'; -import type { PageServerLoad } from '../../../$types'; -import { recoveryCodesTable } from '$lib/server/api/infrastructure/database/tables'; +import { notSignedInMessage } from '$lib/flashMessages' +import { db } from '$lib/server/api/packages/drizzle' +import { eq } from 'drizzle-orm' +import { alphabet, generateRandomString } from 'oslo/crypto' +import { Argon2id } from 'oslo/password' +import { redirect } from 'sveltekit-flash-message/server' +import type { PageServerLoad } from '../../../$types' +import { recoveryCodesTable } from '../../../../../../../lib/server/api/databases/tables' export const load: PageServerLoad = async (event) => { - const { locals } = event; + const { locals } = event - const authedUser = await locals.getAuthedUser(); + const authedUser = await locals.getAuthedUser() if (!authedUser) { - throw redirect(302, '/login', notSignedInMessage, event); + throw redirect(302, '/login', notSignedInMessage, event) } if (authedUser.mfa_enabled) { @@ -42,4 +42,4 @@ export const load: PageServerLoad = async (event) => { } console.error('2FA not enabled') redirect(302, '/profile', { message: 'Two-Factor Authentication is not enabled', type: 'error' }, event) -}; +} diff --git a/src/routes/(app)/(protected)/profile/security/password/change/+page.server.ts b/src/routes/(app)/(protected)/profile/security/password/change/+page.server.ts index ed158f6..c2b9f75 100644 --- a/src/routes/(app)/(protected)/profile/security/password/change/+page.server.ts +++ b/src/routes/(app)/(protected)/profile/security/password/change/+page.server.ts @@ -1,65 +1,65 @@ -import { fail, type Actions } from '@sveltejs/kit'; -import { eq } from 'drizzle-orm'; -import { zod } from 'sveltekit-superforms/adapters'; -import { setError, superValidate } from 'sveltekit-superforms/server'; -import { redirect } from 'sveltekit-flash-message/server'; -import { Argon2id } from 'oslo/password'; -import type { PageServerLoad } from '../../../$types'; -import { db } from '$lib/server/api/infrastructure/database'; -import { changeUserPasswordSchema } from '$lib/validations/account'; -import { usersTable } from '$lib/server/api/infrastructure/database/tables'; -import { notSignedInMessage } from '$lib/flashMessages'; -import type { Cookie } from 'lucia'; +import { notSignedInMessage } from '$lib/flashMessages' +import { db } from '$lib/server/api/packages/drizzle' +import { changeUserPasswordSchema } from '$lib/validations/account' +import { type Actions, fail } from '@sveltejs/kit' +import { eq } from 'drizzle-orm' +import type { Cookie } from 'lucia' +import { Argon2id } from 'oslo/password' +import { redirect } from 'sveltekit-flash-message/server' +import { zod } from 'sveltekit-superforms/adapters' +import { setError, superValidate } from 'sveltekit-superforms/server' +import type { PageServerLoad } from '../../../$types' +import { usersTable } from '../../../../../../../lib/server/api/databases/tables' export const load: PageServerLoad = async (event) => { - const { locals } = event; + const { locals } = event - const authedUser = await locals.getAuthedUser(); + const authedUser = await locals.getAuthedUser() if (!authedUser) { - throw redirect(302, '/login', notSignedInMessage, event); + throw redirect(302, '/login', notSignedInMessage, event) } - const form = await superValidate(event, zod(changeUserPasswordSchema)); + const form = await superValidate(event, zod(changeUserPasswordSchema)) form.data = { current_password: '', password: '', confirm_password: '', - }; + } return { form, - }; -}; + } +} export const actions: Actions = { default: async (event) => { - const { locals } = event; + const { locals } = event - const authedUser = await locals.getAuthedUser(); + const authedUser = await locals.getAuthedUser() if (!authedUser) { - throw redirect(302, '/login', notSignedInMessage, event); + throw redirect(302, '/login', notSignedInMessage, event) } - const form = await superValidate(event, zod(changeUserPasswordSchema)); + const form = await superValidate(event, zod(changeUserPasswordSchema)) if (!form.valid) { return fail(400, { form, - }); + }) } - console.log('updating profile'); + console.log('updating profile') if (!event.locals.user) { - redirect(302, '/login', notSignedInMessage, event); + redirect(302, '/login', notSignedInMessage, event) } if (!event.locals.session) { - return fail(401); + return fail(401) } const dbUser = await db.query.usersTable.findFirst({ where: eq(usersTable.id, authedUser.id), - }); + }) // if (!dbUser?.hashed_password) { // form.data.password = ''; @@ -74,53 +74,50 @@ export const actions: Actions = { const currentPasswordVerified = await new Argon2id().verify( // dbUser.hashed_password, form.data.current_password, - ); + ) if (!currentPasswordVerified) { - return setError(form, 'current_password', 'Your password is incorrect'); + return setError(form, 'current_password', 'Your password is incorrect') } if (authedUser?.username) { - let sessionCookie: Cookie; + let sessionCookie: Cookie try { if (form.data.password !== form.data.confirm_password) { - return setError(form, 'Password and confirm password do not match'); + return setError(form, 'Password and confirm password do not match') } - const hashedPassword = await new Argon2id().hash(form.data.password); - await lucia.invalidateUserSessions(authedUser.id); + const hashedPassword = await new Argon2id().hash(form.data.password) + await lucia.invalidateUserSessions(authedUser.id) // await db // .update(usersTable) // .set({ hashed_password: hashedPassword }) // .where(eq(usersTable.id, user.id)); await lucia.createSession(user.id, { country: event.locals.session?.ipCountry ?? 'unknown', - }); - sessionCookie = lucia.createBlankSessionCookie(); + }) + sessionCookie = lucia.createBlankSessionCookie() } catch (e) { - console.error(e); - form.data.password = ''; - form.data.confirm_password = ''; - form.data.current_password = ''; - return setError(form, 'current_password', 'Your password is incorrect.'); + console.error(e) + form.data.password = '' + form.data.confirm_password = '' + form.data.current_password = '' + return setError(form, 'current_password', 'Your password is incorrect.') } event.cookies.set(sessionCookie.name, sessionCookie.value, { path: '.', ...sessionCookie.attributes, - }); + }) const message = { type: 'success', message: 'Password Updated. Please sign in.', - } as const; - redirect(302, '/login', message, event); + } as const + redirect(302, '/login', message, event) } - return setError( - form, - 'Error occurred. Please try again or contact support if you need further help.', - ); + return setError(form, 'Error occurred. Please try again or contact support if you need further help.') // TODO: Add toast instead? // form.data.password = ''; // form.data.confirm_password = ''; // form.data.current_password = ''; // return message(form, 'Profile updated successfully.'); }, -}; +} diff --git a/src/routes/(app)/(protected)/wishlists/+page.server.ts b/src/routes/(app)/(protected)/wishlists/+page.server.ts index 9c60379..db80d2c 100644 --- a/src/routes/(app)/(protected)/wishlists/+page.server.ts +++ b/src/routes/(app)/(protected)/wishlists/+page.server.ts @@ -1,55 +1,57 @@ -import { fail, error, type Actions } from '@sveltejs/kit'; -import { and, eq } from 'drizzle-orm'; -import { zod } from 'sveltekit-superforms/adapters'; -import { superValidate } from 'sveltekit-superforms/server'; -import { redirect } from 'sveltekit-flash-message/server'; -import { modifyListGameSchema } from '$lib/validations/zod-schemas'; -import db from '../../../../db'; -import { notSignedInMessage } from '$lib/flashMessages.js'; -import { games, wishlist_items, wishlists } from '$db/schema'; -import { userNotAuthenticated } from '$lib/server/auth-utils'; +import { notSignedInMessage } from '$lib/flashMessages.js' +import { games, wishlist_items, wishlists } from '$lib/server/api/databases/tables' +import { db } from '$lib/server/api/packages/drizzle' +import { userNotAuthenticated } from '$lib/server/auth-utils' +import { modifyListGameSchema } from '$lib/validations/zod-schemas' +import { type Actions, error, fail } from '@sveltejs/kit' +import { and, eq } from 'drizzle-orm' +import { redirect } from 'sveltekit-flash-message/server' +import { zod } from 'sveltekit-superforms/adapters' +import { superValidate } from 'sveltekit-superforms/server' export async function load(event) { - const { locals } = event; - const { user, session } = locals; - if (userNotAuthenticated(user, session)) { - redirect(302, '/login', notSignedInMessage, event); + const { locals } = event + + const authedUser = await locals.getAuthedUser() + if (!authedUser) { + throw redirect(302, '/login', notSignedInMessage, event) } const userWishlists = await db.query.wishlists.findMany({ columns: { cuid: true, name: true, - created_at: true, + createdAt: true, }, - where: eq(wishlists.user_id, user!.id!), - }); - console.log('wishlists', userWishlists); + where: eq(wishlists.user_id, authedUser.id), + }) + console.log('wishlists', userWishlists) if (userWishlists?.length === 0) { - console.log('Wishlists not found'); - return fail(404, {}); + console.log('Wishlists not found') + return fail(404, {}) } return { wishlists: userWishlists, - }; + } } export const actions: Actions = { // Add game to a wishlist add: async (event) => { - const { locals } = event; - const { user, session } = locals; - if (userNotAuthenticated(user, session)) { - return fail(401); + const { locals } = event + + const authedUser = await locals.getAuthedUser() + if (!authedUser) { + throw redirect(302, '/login', notSignedInMessage, event) } - const form = await superValidate(event, zod(modifyListGameSchema)); + const form = await superValidate(event, zod(modifyListGameSchema)) try { const game = await db.query.games.findFirst({ where: eq(games.id, form.data.id), - }); + }) if (!game) { // game = await prisma.game.create({ @@ -57,65 +59,66 @@ export const actions: Actions = { // name: form.name // } // }); - console.log('game not found'); - redirect(302, '/404'); + console.log('game not found') + redirect(302, '/404') } if (game) { const wishlist = await db.query.wishlists.findFirst({ - where: eq(wishlists.user_id, user!.id!), - }); + where: eq(wishlists.user_id, authedUser.id), + }) if (!wishlist) { - console.log('Wishlist not found'); - return error(404, 'Wishlist not found'); + console.log('Wishlist not found') + return error(404, 'Wishlist not found') } await db.insert(wishlist_items).values({ game_id: game.id, wishlist_id: wishlist.id, - }); + }) } return { form, - }; + } } catch (e) { - console.error(e); - return error(500, 'Something went wrong'); + console.error(e) + return error(500, 'Something went wrong') } }, // Create new wishlist create: async (event) => { - const { locals } = event; - const { user, session } = locals; + const { locals } = event + const { user, session } = locals if (userNotAuthenticated(user, session)) { - return fail(401); + return fail(401) } - return error(405, 'Method not allowed'); + return error(405, 'Method not allowed') }, // Delete a wishlist delete: async (event) => { - const { locals } = event; - const { user, session } = locals; + const { locals } = event + const { user, session } = locals if (userNotAuthenticated(user, session)) { - return fail(401); + return fail(401) } - return error(405, 'Method not allowed'); + return error(405, 'Method not allowed') }, // Remove game from a wishlist remove: async (event) => { - const { locals } = event; - const { user, session } = locals; - if (userNotAuthenticated(user, session)) { - return fail(401); + const { locals } = event + + const authedUser = await locals.getAuthedUser() + if (!authedUser) { + throw redirect(302, '/login', notSignedInMessage, event) } - const form = await superValidate(event, zod(modifyListGameSchema)); + const form = await superValidate(event, zod(modifyListGameSchema)) try { const game = await db.query.games.findFirst({ where: eq(games.id, form.data.id), - }); + }) if (!game) { // game = await prisma.game.create({ @@ -123,33 +126,29 @@ export const actions: Actions = { // name: form.name // } // }); - console.log('game not found'); - redirect(302, '/404'); + console.log('game not found') + redirect(302, '/404') } if (game) { const wishlist = await db.query.wishlists.findFirst({ - where: eq(wishlists.user_id, user!.id!), - }); + where: eq(wishlists.user_id, authedUser.id), + }) if (!wishlist) { - console.log('Wishlist not found'); - return error(404, 'Wishlist not found'); + console.log('Wishlist not found') + return error(404, 'Wishlist not found') } - await db - .delete(wishlist_items) - .where( - and(eq(wishlist_items.wishlist_id, wishlist.id), eq(wishlist_items.game_id, game.id)), - ); + await db.delete(wishlist_items).where(and(eq(wishlist_items.wishlist_id, wishlist.id), eq(wishlist_items.game_id, game.id))) } return { form, - }; + } } catch (e) { - console.error(e); - return error(500, 'Something went wrong'); + console.error(e) + return error(500, 'Something went wrong') } }, -}; +} diff --git a/src/routes/(app)/(protected)/wishlists/[cuid]/+page.server.ts b/src/routes/(app)/(protected)/wishlists/[cuid]/+page.server.ts index 1e60741..5093585 100644 --- a/src/routes/(app)/(protected)/wishlists/[cuid]/+page.server.ts +++ b/src/routes/(app)/(protected)/wishlists/[cuid]/+page.server.ts @@ -1,64 +1,66 @@ -import { error, type Actions, fail } from '@sveltejs/kit'; -import { and, eq } from 'drizzle-orm'; -import { zod } from 'sveltekit-superforms/adapters'; -import { superValidate } from 'sveltekit-superforms/server'; -import { redirect } from 'sveltekit-flash-message/server'; -import { modifyListGameSchema } from '$lib/validations/zod-schemas'; -import { db } from '$lib/server/api/infrastructure/database'; -import { notSignedInMessage } from '$lib/flashMessages.js'; -import { games, wishlist_items, wishlists } from '$lib/server/api/infrastructure/database/tables'; -import { userNotAuthenticated } from '$lib/server/auth-utils'; +import { notSignedInMessage } from '$lib/flashMessages.js' +import { db } from '$lib/server/api/packages/drizzle' +import { userNotAuthenticated } from '$lib/server/auth-utils' +import { modifyListGameSchema } from '$lib/validations/zod-schemas' +import { type Actions, error, fail } from '@sveltejs/kit' +import { and, eq } from 'drizzle-orm' +import { redirect } from 'sveltekit-flash-message/server' +import { zod } from 'sveltekit-superforms/adapters' +import { superValidate } from 'sveltekit-superforms/server' +import { games, wishlist_items, wishlists } from '../../../../../lib/server/api/databases/tables' export async function load(event) { - const { params, locals } = event; - const { cuid } = params; + const { params, locals } = event + const { cuid } = params - const authedUser = await locals.getAuthedUser(); + const authedUser = await locals.getAuthedUser() if (!authedUser) { - throw redirect(302, '/login', notSignedInMessage, event); + throw redirect(302, '/login', notSignedInMessage, event) } try { - const { data, errors } = await locals.api.wishlists[':cuid'].$get({ - param: { cuid } - }).then(locals.parseApiResponse); + const { data, errors } = await locals.api.wishlists[':cuid'] + .$get({ + param: { cuid }, + }) + .then(locals.parseApiResponse) // const wishlist = await db.query.wishlists.findMany({ // where: and(eq(wishlists.user_id, authedUser.id), eq(wishlists.cuid, cuid)), // }); if (errors) { - return error(500, 'Failed to fetch wishlist'); + return error(500, 'Failed to fetch wishlist') } - const { wishlist } = data; + const { wishlist } = data if (!wishlist) { - redirect(302, '/404'); + redirect(302, '/404') } - console.log('wishlist', wishlist); + console.log('wishlist', wishlist) return { wishlist, - }; + } } catch (e) { - console.error(e); + console.error(e) } - redirect(302, '/404'); + redirect(302, '/404') } export const actions: Actions = { // Add game to a wishlist add: async (event) => { - const { locals } = event; - const { user, session } = locals; + const { locals } = event + const { user, session } = locals if (userNotAuthenticated(user, session)) { - return fail(401); + return fail(401) } - const form = await superValidate(event, zod(modifyListGameSchema)); + const form = await superValidate(event, zod(modifyListGameSchema)) try { const game = await db.query.games.findFirst({ where: eq(games.id, form.data.id), - }); + }) if (!game) { // game = await prisma.game.create({ @@ -66,65 +68,65 @@ export const actions: Actions = { // name: form.name // } // }); - console.log('game not found'); - redirect(302, '/404'); + console.log('game not found') + redirect(302, '/404') } if (game) { const wishlist = await db.query.wishlists.findFirst({ where: eq(wishlists.user_id, user!.id!), - }); + }) if (!wishlist) { - console.log('Wishlist not found'); - return error(404, 'Wishlist not found'); + console.log('Wishlist not found') + return error(404, 'Wishlist not found') } await db.insert(wishlist_items).values({ game_id: game.id, wishlist_id: wishlist.id, - }); + }) } return { form, - }; + } } catch (e) { - console.error(e); - return error(500, 'Something went wrong'); + console.error(e) + return error(500, 'Something went wrong') } }, // Create new wishlist create: async (event) => { - const { locals } = event; - const { user, session } = locals; + const { locals } = event + const { user, session } = locals if (userNotAuthenticated(user, session)) { - return fail(401); + return fail(401) } - return error(405, 'Method not allowed'); + return error(405, 'Method not allowed') }, // Delete a wishlist delete: async (event) => { - const { locals } = event; - const { user, session } = locals; + const { locals } = event + const { user, session } = locals if (userNotAuthenticated(user, session)) { - return fail(401); + return fail(401) } - return error(405, 'Method not allowed'); + return error(405, 'Method not allowed') }, // Remove game from a wishlist remove: async (event) => { - const { locals } = event; - const { user, session } = locals; + const { locals } = event + const { user, session } = locals if (userNotAuthenticated(user, session)) { - return fail(401); + return fail(401) } - const form = await superValidate(event, zod(modifyListGameSchema)); + const form = await superValidate(event, zod(modifyListGameSchema)) try { const game = await db.query.games.findFirst({ where: eq(games.id, form.data.id), - }); + }) if (!game) { // game = await prisma.game.create({ @@ -132,33 +134,29 @@ export const actions: Actions = { // name: form.name // } // }); - console.log('game not found'); - redirect(302, '/404'); + console.log('game not found') + redirect(302, '/404') } if (game) { const wishlist = await db.query.wishlists.findFirst({ where: eq(wishlists.user_id, user!.id!), - }); + }) if (!wishlist) { - console.log('Wishlist not found'); - return error(404, 'Wishlist not found'); + console.log('Wishlist not found') + return error(404, 'Wishlist not found') } - await db - .delete(wishlist_items) - .where( - and(eq(wishlist_items.wishlist_id, wishlist.id), eq(wishlist_items.game_id, game.id)), - ); + await db.delete(wishlist_items).where(and(eq(wishlist_items.wishlist_id, wishlist.id), eq(wishlist_items.game_id, game.id))) } return { form, - }; + } } catch (e) { - console.error(e); - return error(500, 'Something went wrong'); + console.error(e) + return error(500, 'Something went wrong') } }, -}; +} diff --git a/src/routes/(app)/+page.server.ts b/src/routes/(app)/+page.server.ts index 2e54d8b..ad7d0cc 100644 --- a/src/routes/(app)/+page.server.ts +++ b/src/routes/(app)/+page.server.ts @@ -1,23 +1,21 @@ -import { fail } from '@sveltejs/kit'; -import type { MetaTagsProps } from 'svelte-meta-tags'; -import { eq } from 'drizzle-orm'; -import type { PageServerLoad } from './$types'; -import {db} from '$lib/server/api/infrastructure/database/index'; -import { collections, usersTable, wishlists } from '$lib/server/api/infrastructure/database/tables'; +import { db } from '$lib/server/api/packages/drizzle' +import { fail } from '@sveltejs/kit' +import { eq } from 'drizzle-orm' +import type { MetaTagsProps } from 'svelte-meta-tags' +import { collections, usersTable, wishlists } from '../../lib/server/api/databases/tables' +import type { PageServerLoad } from './$types' // import { userFullyAuthenticated } from '$lib/server/auth-utils'; export const load: PageServerLoad = async (event) => { - const { locals, url } = event; + const { locals, url } = event - const authedUser = await locals.getAuthedUser(); + const authedUser = await locals.getAuthedUser() const image = { - url: `${ - new URL(url.pathname, url.origin).href - }og?header=Bored Game&page=Home&content=Keep track of your games`, + url: `${new URL(url.pathname, url.origin).href}og?header=Bored Game&page=Home&content=Keep track of your games`, width: 1200, height: 630, - }; + } const metaTags: MetaTagsProps = Object.freeze({ title: 'Home', description: 'Home page', @@ -36,23 +34,21 @@ export const load: PageServerLoad = async (event) => { cardType: 'summary_large_image', title: 'Home | Bored Game', description: 'Bored Game, keep track of your games', - image: `${ - new URL(url.pathname, url.origin).href - }og?header=Bored Game&page=Home&content=Keep track of your games`, + image: `${new URL(url.pathname, url.origin).href}og?header=Bored Game&page=Home&content=Keep track of your games`, imageAlt: 'Home | Bored Game', }, - }); + }) if (authedUser) { - const { data: wishlistsData, error: wishlistsError } = await locals.api.wishlists.$get().then(locals.parseApiResponse); - const { data: collectionsData, error: collectionsError } = await locals.api.collections.$get().then(locals.parseApiResponse); + const { data: wishlistsData, error: wishlistsError } = await locals.api.wishlists.$get().then(locals.parseApiResponse) + const { data: collectionsData, error: collectionsError } = await locals.api.collections.$get().then(locals.parseApiResponse) if (wishlistsError || collectionsError) { - return fail(500, 'Failed to fetch wishlists or collections'); + return fail(500, 'Failed to fetch wishlists or collections') } - console.log('Wishlists', wishlistsData.wishlists); - console.log('Collections', collectionsData.collections); + console.log('Wishlists', wishlistsData.wishlists) + console.log('Collections', collectionsData.collections) return { metaTagsChild: metaTags, user: { @@ -62,8 +58,8 @@ export const load: PageServerLoad = async (event) => { }, wishlists: wishlistsData.wishlists, collections: collectionsData.collections, - }; + } } - return { metaTagsChild: metaTags, user: null, wishlists: [], collections: [] }; -}; + return { metaTagsChild: metaTags, user: null, wishlists: [], collections: [] } +} diff --git a/src/routes/(app)/game/[id]/+page.server.ts b/src/routes/(app)/game/[id]/+page.server.ts index 7ac273c..8fd02b9 100644 --- a/src/routes/(app)/game/[id]/+page.server.ts +++ b/src/routes/(app)/game/[id]/+page.server.ts @@ -1,26 +1,19 @@ -import { error } from '@sveltejs/kit'; -import { mapAPIGameToBoredGame } from '$lib/utils/gameMapper.js'; -import type { PageServerLoad } from './$types'; -import { createCategory } from '$lib/utils/db/categoryUtils'; -import { createMechanic } from '$lib/utils/db/mechanicUtils'; -import { createPublisher } from '$lib/utils/db/publisherUtils'; -import { createExpansion } from '$lib/utils/db/expansionUtils'; -import { createOrUpdateGame } from '$lib/utils/db/gameUtils'; -import db from '../../../../db'; -import { and, eq } from 'drizzle-orm'; -import { - collection_items, - collections, - expansions, - games, - wishlist_items, - wishlists, -} from '$db/schema'; +import { collection_items, collections, expansions, games, wishlist_items, wishlists } from '$lib/server/api/databases/tables' +import { db } from '$lib/server/api/packages/drizzle' +import { createCategory } from '$lib/utils/db/categoryUtils' +import { createExpansion } from '$lib/utils/db/expansionUtils' +import { createOrUpdateGame } from '$lib/utils/db/gameUtils' +import { createMechanic } from '$lib/utils/db/mechanicUtils' +import { createPublisher } from '$lib/utils/db/publisherUtils' +import { mapAPIGameToBoredGame } from '$lib/utils/gameMapper.js' +import { error } from '@sveltejs/kit' +import { and, eq } from 'drizzle-orm' +import type { PageServerLoad } from './$types' export const load: PageServerLoad = async ({ params, locals, fetch }) => { try { - const { user } = locals; - const { id } = params; + const { user } = locals + const { id } = params const game = await db.query.games.findFirst({ where: eq(games.id, id), with: { @@ -55,20 +48,17 @@ export const load: PageServerLoad = async ({ params, locals, fetch }) => { }, }, }, - }); - console.log('found game', game); + }) + console.log('found game', game) if (!game) { - error(404, 'not found'); + error(404, 'not found') } - const currentDate = new Date(); - if ( - game.last_sync_at === null || - currentDate.getDate() - game.last_sync_at.getDate() > 7 * 24 * 60 * 60 * 1000 - ) { - console.log('Syncing details because last sync is out of date'); - await syncGameAndConnectedData(locals, game, fetch); + const currentDate = new Date() + if (game.last_sync_at === null || currentDate.getDate() - game.last_sync_at.getDate() > 7 * 24 * 60 * 60 * 1000) { + console.log('Syncing details because last sync is out of date') + await syncGameAndConnectedData(locals, game, fetch) } const gameExpansions = await db.query.expansions.findMany({ @@ -82,42 +72,36 @@ export const load: PageServerLoad = async ({ params, locals, fetch }) => { }, }, }, - }); + }) - let collectionItem; - let wishlistItem; + let collectionItem + let wishlistItem if (user) { const wishlist = await db.query.wishlists.findFirst({ where: eq(wishlists.user_id, user.id), - }); + }) // TODO: Select wishlist items based on wishlist if (wishlist) { wishlistItem = await db.query.wishlist_items.findFirst({ - where: and( - eq(wishlist_items.wishlist_id, wishlist.id), - eq(wishlist_items.game_id, game.id), - ), - }); + where: and(eq(wishlist_items.wishlist_id, wishlist.id), eq(wishlist_items.game_id, game.id)), + }) } const collection = await db.query.collections.findFirst({ where: eq(collections.user_id, user.id), - }); + }) // TODO: Select collection items based on collection if (collection) { collectionItem = await db.query.collection_items.findFirst({ - where: and( - eq(collection_items.collection_id, collection.id), - eq(collection_items.game_id, game.id), - ), - }); + where: and(eq(collection_items.collection_id, collection.id), eq(collection_items.game_id, game.id)), + }) } } - console.log('Returning game', game); + console.log('Returning game', game) return { game, @@ -125,55 +109,53 @@ export const load: PageServerLoad = async ({ params, locals, fetch }) => { user, in_wishlist: wishlistItem !== undefined || false, in_collection: collectionItem !== undefined || false, - }; + } } catch (error) { - console.log(error); + console.log(error) } - error(404, 'not found'); -}; + error(404, 'not found') +} async function syncGameAndConnectedData(locals: App.Locals, game: Game, eventFetch: Function) { - console.log( - `Retrieving full external game details for external id: ${game.external_id} with name ${game.name}`, - ); - const externalGameResponse = await eventFetch(`/api/external/game/${game.external_id}`); + console.log(`Retrieving full external game details for external id: ${game.external_id} with name ${game.name}`) + const externalGameResponse = await eventFetch(`/api/external/game/${game.external_id}`) if (externalGameResponse.ok) { - const externalGame = await externalGameResponse.json(); - console.log('externalGame', externalGame); - const categories = []; - const mechanics = []; - const publishers = []; + const externalGame = await externalGameResponse.json() + console.log('externalGame', externalGame) + const categories = [] + const mechanics = [] + const publishers = [] for (const externalCategory of externalGame.categories) { - const category = await createCategory(locals, externalCategory, externalGame.external_id); + const category = await createCategory(locals, externalCategory, externalGame.external_id) categories.push({ id: category.id, - }); + }) } for (const externalMechanic of externalGame.mechanics) { - const mechanic = await createMechanic(locals, externalMechanic, externalGame.external_id); - mechanics.push({ id: mechanic.id }); + const mechanic = await createMechanic(locals, externalMechanic, externalGame.external_id) + mechanics.push({ id: mechanic.id }) } for (const externalPublisher of externalGame.publishers) { - const publisher = await createPublisher(locals, externalPublisher, externalGame.external_id); - publishers.push({ id: publisher.id }); + const publisher = await createPublisher(locals, externalPublisher, externalGame.external_id) + publishers.push({ id: publisher.id }) } for (const externalExpansion of externalGame.expansions) { - console.log('Inbound?', externalExpansion.inbound); + console.log('Inbound?', externalExpansion.inbound) if (externalExpansion?.inbound === true) { - createExpansion(locals, externalExpansion); + createExpansion(locals, externalExpansion) } else { - createExpansion(locals, externalExpansion); + createExpansion(locals, externalExpansion) } } - const boredGame = mapAPIGameToBoredGame(externalGame); + const boredGame = mapAPIGameToBoredGame(externalGame) - boredGame.categories = categories; - boredGame.mechanics = mechanics; - boredGame.publishers = publishers; + boredGame.categories = categories + boredGame.mechanics = mechanics + boredGame.publishers = publishers // boredGame.expansions = expansions; - return createOrUpdateGame(locals, boredGame, externalGame.external_id); + return createOrUpdateGame(locals, boredGame, externalGame.external_id) } } diff --git a/src/routes/(auth)/login/+page.server.ts b/src/routes/(auth)/login/+page.server.ts index b5f7757..ab0a9a1 100644 --- a/src/routes/(auth)/login/+page.server.ts +++ b/src/routes/(auth)/login/+page.server.ts @@ -1,14 +1,14 @@ -import { fail, type Actions } from '@sveltejs/kit' +import { signinUsernameDto } from '$lib/dtos/signin-username.dto' +import { db } from '$lib/server/api/packages/drizzle' +import { lucia } from '$lib/server/api/packages/lucia' +import { type Actions, fail } from '@sveltejs/kit' import { eq, or } from 'drizzle-orm' import { Argon2id } from 'oslo/password' +import { redirect } from 'sveltekit-flash-message/server' import { zod } from 'sveltekit-superforms/adapters' import { setError, superValidate } from 'sveltekit-superforms/server' -import { redirect } from 'sveltekit-flash-message/server' -import { db } from '../../../lib/server/api/infrastructure/database/index' -import { lucia } from '../../../lib/server/api/infrastructure/auth/lucia' -import { credentialsTable, usersTable } from '../../../lib/server/api/infrastructure/database/tables' +import { credentialsTable, usersTable } from '../../../lib/server/api/databases/tables' import type { PageServerLoad } from './$types' -import { signinUsernameDto } from '$lib/dtos/signin-username.dto' export const load: PageServerLoad = async (event) => { const { locals } = event diff --git a/src/routes/(auth)/signup/+page.server.ts b/src/routes/(auth)/signup/+page.server.ts index abc18b3..593388f 100644 --- a/src/routes/(auth)/signup/+page.server.ts +++ b/src/routes/(auth)/signup/+page.server.ts @@ -1,9 +1,9 @@ -import { fail, error, type Actions } from '@sveltejs/kit'; -import { zod } from 'sveltekit-superforms/adapters'; -import { setError, superValidate } from 'sveltekit-superforms/server'; -import { redirect } from 'sveltekit-flash-message/server'; -import type { PageServerLoad } from './$types'; -import {signupUsernameEmailDto} from "$lib/dtos/signup-username-email.dto"; +import { signupUsernameEmailDto } from '$lib/dtos/signup-username-email.dto' +import { type Actions, error, fail } from '@sveltejs/kit' +import { redirect } from 'sveltekit-flash-message/server' +import { zod } from 'sveltekit-superforms/adapters' +import { setError, superValidate } from 'sveltekit-superforms/server' +import type { PageServerLoad } from './$types' const signUpDefaults = { firstName: '', @@ -13,16 +13,16 @@ const signUpDefaults = { password: '', confirm_password: '', terms: true, -}; +} export const load: PageServerLoad = async (event) => { - const { locals } = event; + const { locals } = event - const authedUser = await locals.getAuthedUser(); + const authedUser = await locals.getAuthedUser() if (authedUser) { - const message = { type: 'success', message: 'You are already signed in' } as const; - throw redirect('/', message, event); + const message = { type: 'success', message: 'You are already signed in' } as const + throw redirect('/', message, event) } // if (userFullyAuthenticated(user, session)) { @@ -45,31 +45,31 @@ export const load: PageServerLoad = async (event) => { form: await superValidate(zod(signupUsernameEmailDto), { defaults: signUpDefaults, }), - }; -}; + } +} export const actions: Actions = { default: async (event) => { - const { locals } = event; + const { locals } = event - const authedUser = await locals.getAuthedUser(); + const authedUser = await locals.getAuthedUser() if (authedUser) { - const message = { type: 'success', message: 'You are already signed in' } as const; - throw redirect('/', message, event); + const message = { type: 'success', message: 'You are already signed in' } as const + throw redirect('/', message, event) } - const form = await superValidate(event, zod(signupUsernameEmailDto)); + const form = await superValidate(event, zod(signupUsernameEmailDto)) - const { error } = await locals.api.signup.$post({ json: form.data }).then(locals.parseApiResponse); - if (error) return setError(form, 'username', error); + const { error } = await locals.api.signup.$post({ json: form.data }).then(locals.parseApiResponse) + if (error) return setError(form, 'username', error) if (!form.valid) { - form.data.password = ''; - form.data.confirm_password = ''; + form.data.password = '' + form.data.confirm_password = '' return fail(400, { form, - }); + }) } // let session; @@ -147,8 +147,8 @@ export const actions: Actions = { // ...sessionCookie.attributes, // }); - redirect(302, '/'); + redirect(302, '/') // const message = { type: 'success', message: 'Signed Up!' } as const; // throw flashRedirect(message, event); }, -}; +} diff --git a/src/routes/(auth)/signup/+page.svelte b/src/routes/(auth)/signup/+page.svelte index 54c563b..d0f2491 100644 --- a/src/routes/(auth)/signup/+page.svelte +++ b/src/routes/(auth)/signup/+page.svelte @@ -1,39 +1,39 @@ diff --git a/src/routes/(auth)/totp/+page.server.ts b/src/routes/(auth)/totp/+page.server.ts index 4e6f719..f6dc984 100644 --- a/src/routes/(auth)/totp/+page.server.ts +++ b/src/routes/(auth)/totp/+page.server.ts @@ -1,326 +1,297 @@ -import { fail, error, type Actions } from '@sveltejs/kit'; -import { and, eq } from 'drizzle-orm'; -import { Argon2id } from 'oslo/password'; -import { decodeHex } from 'oslo/encoding'; -import { TOTPController } from 'oslo/otp'; -import { zod } from 'sveltekit-superforms/adapters'; -import { setError, superValidate } from 'sveltekit-superforms/server'; -import { redirect } from 'sveltekit-flash-message/server'; -import { RateLimiter } from 'sveltekit-rate-limiter/server'; -import db from '../../../db'; -import { lucia } from '$lib/server/auth'; -import { recoveryCodeSchema, totpSchema } from '$lib/validations/auth'; -import { usersTable, twoFactor, recoveryCodes } from '$db/schema'; -import type {PageServerLoad, RequestEvent} from './$types'; -import { notSignedInMessage } from '$lib/flashMessages'; -import env from '../../../env'; +import { notSignedInMessage } from '$lib/flashMessages' +import { recoveryCodesTable, twoFactorTable, usersTable } from '$lib/server/api/databases/tables' +import { db } from '$lib/server/api/packages/drizzle' +import { lucia } from '$lib/server/api/packages/lucia' +import { recoveryCodeSchema, totpSchema } from '$lib/validations/auth' +import { type Actions, error, fail } from '@sveltejs/kit' +import { and, eq } from 'drizzle-orm' +import { decodeHex } from 'oslo/encoding' +import { TOTPController } from 'oslo/otp' +import { Argon2id } from 'oslo/password' +import { redirect } from 'sveltekit-flash-message/server' +import { RateLimiter } from 'sveltekit-rate-limiter/server' +import { zod } from 'sveltekit-superforms/adapters' +import { setError, superValidate } from 'sveltekit-superforms/server' +import env from '../../../env' +import type { PageServerLoad, RequestEvent } from './$types' export const load: PageServerLoad = async (event) => { - const { cookies, locals } = event; - const { user, session } = locals; + const { cookies, locals } = event - if (!user || !session) { - redirect(302, '/login', notSignedInMessage, event); + const authedUser = await locals.getAuthedUser() + if (!authedUser) { + throw redirect(302, '/login', notSignedInMessage, event) } - if (user && session) { - const dbUser = await db.query.usersTable.findFirst({ - where: eq(usersTable.username, user.username), - }); + const dbUser = await db.query.usersTable.findFirst({ + where: eq(usersTable.username, authedUser.username), + }) - const twoFactorDetails = await db.query.twoFactor.findFirst({ - where: eq(twoFactor.userId, dbUser!.id!), - }); + const twoFactorDetails = await db.query.twoFactorTable.findFirst({ + where: eq(twoFactorTable.userId, authedUser.id), + }) - if (!twoFactorDetails || !twoFactorDetails.enabled) { - const message = { - type: 'error', - message: 'Two factor authentication is not enabled', - } as const; - redirect(302, '/login', message, event); - } - - let twoFactorInitiatedTime = twoFactorDetails.initiatedTime; - if (twoFactorInitiatedTime === null) { - console.log('twoFactorInitiatedTime is null'); - twoFactorInitiatedTime = new Date(); - console.log('twoFactorInitiatedTime', twoFactorInitiatedTime); - await db - .update(twoFactor) - .set({ initiatedTime: twoFactorInitiatedTime }) - .where(eq(twoFactor.userId, dbUser!.id!)); - } - - // Check if two factor started less than TWO_FACTOR_TIMEOUT - const totpElapsed = totpTimeElapsed(twoFactorInitiatedTime); - if (totpElapsed) { - console.log( - 'Time elapsed was more than TWO_FACTOR_TIMEOUT', - totpElapsed, - env.TWO_FACTOR_TIMEOUT, - ); - await lucia.invalidateSession(session!.id!); - const sessionCookie = lucia.createBlankSessionCookie(); - cookies.set(sessionCookie.name, sessionCookie.value, { - path: '.', - ...sessionCookie.attributes, - }); - const message = { type: 'error', message: 'Two factor authentication has expired' } as const; - redirect(302, '/login', message, event); - } - - const isTwoFactorAuthenticated = session?.isTwoFactorAuthenticated; - - console.log('session', session); - console.log('isTwoFactorAuthenticated', isTwoFactorAuthenticated); - - if (isTwoFactorAuthenticated && twoFactorDetails?.enabled && twoFactorDetails?.secret !== '') { - const message = { type: 'success', message: 'You are already signed in' } as const; - throw redirect('/', message, event); - } + if (!twoFactorDetails || !twoFactorDetails.enabled) { + const message = { + type: 'error', + message: 'Two factor authentication is not enabled', + } as const + redirect(302, '/login', message, event) } + let twoFactorInitiatedTime = twoFactorDetails.initiatedTime + if (twoFactorInitiatedTime === null) { + console.log('twoFactorInitiatedTime is null') + twoFactorInitiatedTime = new Date() + console.log('twoFactorInitiatedTime', twoFactorInitiatedTime) + await db.update(twoFactorTable).set({ initiatedTime: twoFactorInitiatedTime }).where(eq(twoFactorTable.userId, dbUser!.id!)) + } + + // Check if two factor started less than TWO_FACTOR_TIMEOUT + // const totpElapsed = totpTimeElapsed(twoFactorInitiatedTime) + // if (totpElapsed) { + // console.log('Time elapsed was more than TWO_FACTOR_TIMEOUT', totpElapsed, env.TWO_FACTOR_TIMEOUT) + // await lucia.invalidateSession(session!.id!) + // const sessionCookie = lucia.createBlankSessionCookie() + // cookies.set(sessionCookie.name, sessionCookie.value, { + // path: '.', + // ...sessionCookie.attributes, + // }) + // const message = { type: 'error', message: 'Two factor authentication has expired' } as const + // redirect(302, '/login', message, event) + // } + // + // const isTwoFactorAuthenticated = session?.isTwoFactorAuthenticated + // + // console.log('session', session) + // console.log('isTwoFactorAuthenticated', isTwoFactorAuthenticated) + + // if (isTwoFactorAuthenticated && twoFactorDetails?.enabled && twoFactorDetails?.secret !== '') { + // const message = { type: 'success', message: 'You are already signed in' } as const + // throw redirect('/', message, event) + // } + return { totpForm: await superValidate(event, zod(totpSchema)), recoveryCodeForm: await superValidate(event, zod(recoveryCodeSchema)), - }; -}; + } +} const limiter = new RateLimiter({ // A rate is defined by [number, unit] IPUA: [5, 'm'], -}); +}) export const actions: Actions = { validateTotp: async (event) => { - const { cookies, locals } = event; - const session = locals.session; - const user = locals.user; + const { cookies, locals } = event - if (await limiter.isLimited(event)) { - throw error(429); + const authedUser = await locals.getAuthedUser() + if (!authedUser) { + throw redirect(302, '/login', notSignedInMessage, event) } - if (!user || !session) { - throw fail(401); - } + const { dbUser, twoFactorDetails } = await validateUserData(event, locals) - const { dbUser, twoFactorDetails } = await validateUserData(event, locals); - - const totpForm = await superValidate(event, zod(totpSchema)); + const totpForm = await superValidate(event, zod(totpSchema)) if (!totpForm.valid) { - totpForm.data.totpToken = ''; - return fail(400, { totpForm }); + totpForm.data.totpToken = '' + return fail(400, { totpForm }) } - let sessionCookie; - const totpToken = totpForm?.data?.totpToken; - - const twoFactorSecretPopulated = - twoFactorDetails.secret !== '' && twoFactorDetails.secret !== null; - if (twoFactorDetails.enabled && !twoFactorSecretPopulated && !totpToken) { - return fail(400, { totpForm }); - } else if (twoFactorSecretPopulated && totpToken) { - // Check if two factor started less than TWO_FACTOR_TIMEOUT - const totpElapsed = totpTimeElapsed(twoFactorDetails.initiatedTime ?? new Date()); - if (totpElapsed) { - await lucia.invalidateSession(session!.id!); - const sessionCookie = lucia.createBlankSessionCookie(); - cookies.set(sessionCookie.name, sessionCookie.value, { - path: '.', - ...sessionCookie.attributes, - }); - const message = { - type: 'error', - message: 'Two factor authentication has expired', - } as const; - redirect(302, '/login', message, event); - } - - console.log('totpToken', totpToken); - const validOTP = await new TOTPController().verify( - totpToken, - decodeHex(twoFactorDetails.secret ?? ''), - ); - console.log('validOTP', validOTP); - - if (!validOTP) { - console.log('invalid TOTP code'); - totpForm.data.totpToken = ''; - return setError(totpForm, 'totpToken', 'Invalid code.'); - } - } - console.log('ip', locals.ip); - console.log('country', locals.country); - await lucia.invalidateSession(session.id); - const newSession = await lucia.createSession(dbUser.id, { - ip_country: locals.country, - ip_address: locals.ip, - twoFactorAuthEnabled: true, - isTwoFactorAuthenticated: true, - }); - console.log('logging in session', newSession); - sessionCookie = lucia.createSessionCookie(newSession.id); - console.log('logging in session cookie', sessionCookie); - - console.log('setting session cookie', sessionCookie); - event.cookies.set(sessionCookie.name, sessionCookie.value, { - path: '.', - ...sessionCookie.attributes, - }); - - totpForm.data.totpToken = ''; - const message = { type: 'success', message: 'Signed In!' } as const; - redirect(302, '/', message, event); + // let sessionCookie + // const totpToken = totpForm?.data?.totpToken + // + // const twoFactorSecretPopulated = twoFactorDetails.secret !== '' && twoFactorDetails.secret !== null + // if (twoFactorDetails.enabled && !twoFactorSecretPopulated && !totpToken) { + // return fail(400, { totpForm }) + // } else if (twoFactorSecretPopulated && totpToken) { + // // Check if two factor started less than TWO_FACTOR_TIMEOUT + // const totpElapsed = totpTimeElapsed(twoFactorDetails.initiatedTime ?? new Date()) + // if (totpElapsed) { + // await lucia.invalidateSession(session!.id!) + // const sessionCookie = lucia.createBlankSessionCookie() + // cookies.set(sessionCookie.name, sessionCookie.value, { + // path: '.', + // ...sessionCookie.attributes, + // }) + // const message = { + // type: 'error', + // message: 'Two factor authentication has expired', + // } as const + // redirect(302, '/login', message, event) + // } + // + // console.log('totpToken', totpToken) + // const validOTP = await new TOTPController().verify(totpToken, decodeHex(twoFactorDetails.secret ?? '')) + // console.log('validOTP', validOTP) + // + // if (!validOTP) { + // console.log('invalid TOTP code') + // totpForm.data.totpToken = '' + // return setError(totpForm, 'totpToken', 'Invalid code.') + // } + // } + // console.log('ip', locals.ip) + // console.log('country', locals.country) + // await lucia.invalidateSession(session.id) + // const newSession = await lucia.createSession(dbUser.id, { + // ip_country: locals.country, + // ip_address: locals.ip, + // twoFactorAuthEnabled: true, + // isTwoFactorAuthenticated: true, + // }) + // console.log('logging in session', newSession) + // sessionCookie = lucia.createSessionCookie(newSession.id) + // console.log('logging in session cookie', sessionCookie) + // + // console.log('setting session cookie', sessionCookie) + // event.cookies.set(sessionCookie.name, sessionCookie.value, { + // path: '.', + // ...sessionCookie.attributes, + // }) + // + // totpForm.data.totpToken = '' + // const message = { type: 'success', message: 'Signed In!' } as const + redirect(302, '/', message, event) }, validateRecoveryCode: async (event) => { - const { cookies, locals } = event; - const session = locals.session; - const user = locals.user; + const { cookies, locals } = event - if (await limiter.isLimited(event)) { - throw error(429); + const authedUser = await locals.getAuthedUser() + if (!authedUser) { + throw redirect(302, '/login', notSignedInMessage, event) } - if (!user || !session) { - throw fail(401); - } + const { dbUser, twoFactorDetails } = await validateUserData(event, locals) - const { dbUser, twoFactorDetails } = await validateUserData(event, locals); - - const recoveryCodeForm = await superValidate(event, zod(recoveryCodeSchema)); + const recoveryCodeForm = await superValidate(event, zod(recoveryCodeSchema)) if (!recoveryCodeForm.valid) { return fail(400, { form: recoveryCodeForm, - }); + }) } - let sessionCookie; - const recoveryCode = recoveryCodeForm?.data?.recoveryCode; + // let sessionCookie + // const recoveryCode = recoveryCodeForm?.data?.recoveryCode + // + // const twoFactorSecretPopulated = twoFactorDetails.secret !== '' && twoFactorDetails.secret !== null + // if (twoFactorDetails.enabled && !twoFactorSecretPopulated && !recoveryCode) { + // return fail(400, { recoveryCodeForm }) + // } else if (twoFactorSecretPopulated && recoveryCode) { + // // Check if two factor started less than TWO_FACTOR_TIMEOUT + // const totpElapsed = totpTimeElapsed(twoFactorDetails.initiatedTime ?? new Date()) + // if (totpElapsed) { + // await lucia.invalidateSession(session!.id!) + // const sessionCookie = lucia.createBlankSessionCookie() + // cookies.set(sessionCookie.name, sessionCookie.value, { + // path: '.', + // ...sessionCookie.attributes, + // }) + // const message = { + // type: 'error', + // message: 'Two factor authentication has expired', + // } as const + // redirect(302, '/login', message, event) + // } + // + // console.log('recoveryCode', recoveryCode) + // + // console.log('Check for recovery codes') + // const usedRecoveryCode = await checkRecoveryCode(recoveryCode, dbUser.id) + // if (!usedRecoveryCode) { + // console.log('invalid recovery code') + // recoveryCodeForm.data.recoveryCode = '' + // return setError(recoveryCodeForm, 'recoveryCode', 'Invalid code.') + // } + // } + // console.log('ip', locals.ip) + // console.log('country', locals.country) + // await lucia.invalidateSession(session.id) + // const newSession = await lucia.createSession(dbUser.id, { + // ip_country: locals.country, + // ip_address: locals.ip, + // twoFactorAuthEnabled: true, + // isTwoFactorAuthenticated: true, + // }) + // console.log('logging in session', newSession) + // sessionCookie = lucia.createSessionCookie(newSession.id) + // console.log('logging in session cookie', sessionCookie) + // + // console.log('setting session cookie', sessionCookie) + // event.cookies.set(sessionCookie.name, sessionCookie.value, { + // path: '.', + // ...sessionCookie.attributes, + // }) - const twoFactorSecretPopulated = - twoFactorDetails.secret !== '' && twoFactorDetails.secret !== null; - if (twoFactorDetails.enabled && !twoFactorSecretPopulated && !recoveryCode) { - return fail(400, { recoveryCodeForm }); - } else if (twoFactorSecretPopulated && recoveryCode) { - // Check if two factor started less than TWO_FACTOR_TIMEOUT - const totpElapsed = totpTimeElapsed(twoFactorDetails.initiatedTime ?? new Date()); - if (totpElapsed) { - await lucia.invalidateSession(session!.id!); - const sessionCookie = lucia.createBlankSessionCookie(); - cookies.set(sessionCookie.name, sessionCookie.value, { - path: '.', - ...sessionCookie.attributes, - }); - const message = { - type: 'error', - message: 'Two factor authentication has expired', - } as const; - redirect(302, '/login', message, event); - } - - console.log('recoveryCode', recoveryCode); - - console.log('Check for recovery codes'); - const usedRecoveryCode = await checkRecoveryCode(recoveryCode, dbUser.id); - if (!usedRecoveryCode) { - console.log('invalid recovery code'); - recoveryCodeForm.data.recoveryCode = ''; - return setError(recoveryCodeForm, 'recoveryCode', 'Invalid code.'); - } - } - console.log('ip', locals.ip); - console.log('country', locals.country); - await lucia.invalidateSession(session.id); - const newSession = await lucia.createSession(dbUser.id, { - ip_country: locals.country, - ip_address: locals.ip, - twoFactorAuthEnabled: true, - isTwoFactorAuthenticated: true, - }); - console.log('logging in session', newSession); - sessionCookie = lucia.createSessionCookie(newSession.id); - console.log('logging in session cookie', sessionCookie); - - console.log('setting session cookie', sessionCookie); - event.cookies.set(sessionCookie.name, sessionCookie.value, { - path: '.', - ...sessionCookie.attributes, - }); - - recoveryCodeForm.data.recoveryCode = ''; - const message = { type: 'success', message: 'Signed In!' } as const; - redirect(302, '/', message, event); - } -}; + recoveryCodeForm.data.recoveryCode = '' + const message = { type: 'success', message: 'Signed In!' } as const + redirect(302, '/', message, event) + }, +} async function validateUserData(event: RequestEvent, locals: App.Locals) { - const { user, session } = locals; + const { user, session } = locals if (!user || !session) { - throw fail(401); + throw fail(401) } const dbUser = await db.query.usersTable.findFirst({ where: eq(usersTable.username, user.username), - }); + }) if (!dbUser) { - throw fail(401); + throw fail(401) } - const isTwoFactorAuthenticated = session?.isTwoFactorAuthenticated; - const twoFactorDetails = await db.query.twoFactor.findFirst({ - where: eq(twoFactor.userId, dbUser!.id!), - }); + const isTwoFactorAuthenticated = session?.isTwoFactorAuthenticated + const twoFactorDetails = await db.query.twoFactorTable.findFirst({ + where: eq(twoFactorTable.userId, dbUser!.id!), + }) if (!twoFactorDetails) { - const message = {type: 'error', message: 'Unable to process request'} as const; - throw redirect(302, '/login', message, event); + const message = { type: 'error', message: 'Unable to process request' } as const + throw redirect(302, '/login', message, event) } if (isTwoFactorAuthenticated && twoFactorDetails.enabled && twoFactorDetails.secret !== '') { - const message = {type: 'success', message: 'You are already signed in'} as const; - throw redirect('/', message, event); + const message = { type: 'success', message: 'You are already signed in' } as const + throw redirect('/', message, event) } - return { dbUser, twoFactorDetails }; + return { dbUser, twoFactorDetails } } - function totpTimeElapsed(initiatedTime: Date) { if (initiatedTime === null || initiatedTime === undefined) { - return true; + return true } - const timeElapsed = Date.now() - initiatedTime.getTime(); - console.log('Time elapsed', timeElapsed); + const timeElapsed = Date.now() - initiatedTime.getTime() + console.log('Time elapsed', timeElapsed) if (timeElapsed > env.TWO_FACTOR_TIMEOUT) { - console.log( - 'Time elapsed was more than TWO_FACTOR_TIMEOUT', - timeElapsed, - env.TWO_FACTOR_TIMEOUT, - ); - return true; + console.log('Time elapsed was more than TWO_FACTOR_TIMEOUT', timeElapsed, env.TWO_FACTOR_TIMEOUT) + return true } - return false; + return false } async function checkRecoveryCode(recoveryCode: string, userId: string) { - const userRecoveryCodes = await db.query.recoveryCodes.findMany({ - where: and(eq(recoveryCodes.used, false), eq(recoveryCodes.userId, userId)), - }); + const userRecoveryCodes = await db.query.recoveryCodesTable.findMany({ + where: and(eq(recoveryCodesTable.used, false), eq(recoveryCodesTable.userId, userId)), + }) for (const code of userRecoveryCodes) { - const validRecoveryCode = await new Argon2id().verify(code.code, recoveryCode); + const validRecoveryCode = await new Argon2id().verify(code.code, recoveryCode) if (validRecoveryCode) { await db - .update(recoveryCodes) + .update(recoveryCodesTable) .set({ used: true, }) - .where(eq(recoveryCodes.id, code.id)); - return true; + .where(eq(recoveryCodesTable.id, code.id)) + return true } } - return false; + return false }