diff --git a/.env.example b/.env.example index b7afaf0..c2fdb5f 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,15 @@ +# API ORIGIN=http://localhost:5173 # Database DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres -REDIS_URL=redis://localhost:6379 \ No newline at end of file + +# Redis +REDIS_URL=redis://localhost:6379 + +# Storage +PUBLIC_IMAGE_URI=http://localhost:9000/dev +STORAGE_BUCKET=dev +STORAGE_URL=http://localhost:9000 +STORAGE_ACCESS_KEY=user +STORAGE_SECRET_KEY=password diff --git a/drizzle.config.ts b/drizzle.config.ts index df55597..5ec2892 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,8 +1,8 @@ import type { Config } from 'drizzle-kit'; export default { - out: './src/lib/server/api/databases/migrations', - schema: './src/lib/server/api/databases/tables/*.table.ts', + out: './src/lib/server/api/databases/postgres/migrations', + schema: './src/lib/server/api/databases/postgres/tables/*.table.ts', breakpoints: false, strict: true, dialect: 'postgresql', diff --git a/package.json b/package.json index 3046604..3785b80 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,8 @@ "mode-watcher": "^0.4.1", "paneforge": "^0.0.5", "rate-limit-redis": "^4.2.0", + "redis": "^4.7.0", + "redis-om": "^0.4.6", "resend": "^3.5.0", "svelte-sonner": "^0.3.27", "tailwind-merge": "^2.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 756a403..4c81519 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,12 @@ importers: rate-limit-redis: specifier: ^4.2.0 version: 4.2.0(express-rate-limit@7.4.0(express@4.19.2)) + redis: + specifier: ^4.7.0 + version: 4.7.0 + redis-om: + specifier: ^0.4.6 + version: 0.4.6 resend: specifier: ^3.5.0 version: 3.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1416,6 +1422,35 @@ packages: react: ^18.2.0 react-dom: ^18.2.0 + '@redis/bloom@1.2.0': + resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/client@1.6.0': + resolution: {integrity: sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==} + engines: {node: '>=14'} + + '@redis/graph@1.1.1': + resolution: {integrity: sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/json@1.0.7': + resolution: {integrity: sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/search@1.2.0': + resolution: {integrity: sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/time-series@1.1.0': + resolution: {integrity: sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==} + peerDependencies: + '@redis/client': ^1.0.0 + '@rollup/plugin-commonjs@26.0.1': resolution: {integrity: sha512-UnsKoZK6/aGIH6AdkptXhNvhaqftcjq3zZdT+LY5Ftms6JR06nADcDsYp5hTU9E2lbJUEOhdlY5J4DNTneM+jQ==} engines: {node: '>=16.0.0 || 14 >= 14.17'} @@ -2638,6 +2673,10 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + generic-pool@3.9.0: + resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} + engines: {node: '>= 4'} + get-func-name@2.0.2: resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} @@ -2872,6 +2911,10 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + jsonpath-plus@7.2.0: + resolution: {integrity: sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==} + engines: {node: '>=12.0.0'} + just-clone@6.2.0: resolution: {integrity: sha512-1IynUYEc/HAwxhi3WDpIpxJbZpMCvvrrmZVqvj9EhpvbH8lls7HhdhiByjL7DkAaWlLIzpC0Xc/VPvy/UxLNjA==} @@ -3494,10 +3537,17 @@ packages: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} + redis-om@0.4.6: + resolution: {integrity: sha512-L6cfZfG+I7ES+hHfBBKQwUEbfmGQJhIvcreP5NgxkxX+LtYLLXrcpP/sIIW1jMyjdgJE1KRjAbiyiuL2AAHe3g==} + engines: {node: '>= 14'} + redis-parser@3.0.0: resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} engines: {node: '>=4'} + redis@4.7.0: + resolution: {integrity: sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==} + reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} @@ -3883,6 +3933,10 @@ packages: engines: {node: '>=14.17'} hasBin: true + ulid@2.3.0: + resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==} + hasBin: true + undici-types@6.13.0: resolution: {integrity: sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==} @@ -4024,6 +4078,9 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yaml@1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} @@ -5252,6 +5309,32 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-promise-suspense: 0.3.4 + '@redis/bloom@1.2.0(@redis/client@1.6.0)': + dependencies: + '@redis/client': 1.6.0 + + '@redis/client@1.6.0': + dependencies: + cluster-key-slot: 1.1.2 + generic-pool: 3.9.0 + yallist: 4.0.0 + + '@redis/graph@1.1.1(@redis/client@1.6.0)': + dependencies: + '@redis/client': 1.6.0 + + '@redis/json@1.0.7(@redis/client@1.6.0)': + dependencies: + '@redis/client': 1.6.0 + + '@redis/search@1.2.0(@redis/client@1.6.0)': + dependencies: + '@redis/client': 1.6.0 + + '@redis/time-series@1.1.0(@redis/client@1.6.0)': + dependencies: + '@redis/client': 1.6.0 + '@rollup/plugin-commonjs@26.0.1(rollup@4.20.0)': dependencies: '@rollup/pluginutils': 5.1.0(rollup@4.20.0) @@ -6690,6 +6773,8 @@ snapshots: function-bind@1.1.2: {} + generic-pool@3.9.0: {} + get-func-name@2.0.2: {} get-intrinsic@1.2.4: @@ -6945,6 +7030,8 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + jsonpath-plus@7.2.0: {} + just-clone@6.2.0: {} keyv@4.5.4: @@ -7428,10 +7515,26 @@ snapshots: redis-errors@1.2.0: {} + redis-om@0.4.6: + dependencies: + jsonpath-plus: 7.2.0 + just-clone: 6.2.0 + redis: 4.7.0 + ulid: 2.3.0 + redis-parser@3.0.0: dependencies: redis-errors: 1.2.0 + redis@4.7.0: + dependencies: + '@redis/bloom': 1.2.0(@redis/client@1.6.0) + '@redis/client': 1.6.0 + '@redis/graph': 1.1.1(@redis/client@1.6.0) + '@redis/json': 1.0.7(@redis/client@1.6.0) + '@redis/search': 1.2.0(@redis/client@1.6.0) + '@redis/time-series': 1.1.0(@redis/client@1.6.0) + reflect-metadata@0.2.2: {} regenerator-runtime@0.14.1: @@ -7870,6 +7973,8 @@ snapshots: typescript@5.5.4: {} + ulid@2.3.0: {} + undici-types@6.13.0: {} unpipe@1.0.0: {} @@ -7995,6 +8100,8 @@ snapshots: xtend@4.0.2: {} + yallist@4.0.0: {} + yaml@1.10.2: {} yaml@2.5.0: {} diff --git a/src/lib/server/api/common/config.ts b/src/lib/server/api/common/config.ts new file mode 100644 index 0000000..cd60258 --- /dev/null +++ b/src/lib/server/api/common/config.ts @@ -0,0 +1,22 @@ +import * as envs from '$env/static/private'; +import type { Config } from './types/config.type'; + +export const config: Config = { + isProduction: envs.NODE_ENV === 'production', + api: { + origin: envs.ORIGIN, + }, + storage: { + accessKey: envs.STORAGE_ACCESS_KEY, + secretKey: envs.STORAGE_SECRET_KEY, + bucket: envs.STORAGE_BUCKET, + url: envs.STORAGE_URL + }, + postgres: { + url: envs.DATABASE_URL + }, + redis: { + url: envs.REDIS_URL + } +} + diff --git a/src/lib/server/api/common/inferfaces/repository.interface.ts b/src/lib/server/api/common/inferfaces/repository.interface.ts deleted file mode 100644 index 091a78d..0000000 --- a/src/lib/server/api/common/inferfaces/repository.interface.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { DatabaseProvider } from "../../providers/database.provider"; - -export interface Repository { - trxHost(trx: DatabaseProvider): any; -} diff --git a/src/lib/server/api/common/types/async-service.ts b/src/lib/server/api/common/types/async-service.ts new file mode 100644 index 0000000..d85364c --- /dev/null +++ b/src/lib/server/api/common/types/async-service.ts @@ -0,0 +1,3 @@ +export abstract class AsyncService { + async init(): Promise { } +} \ No newline at end of file diff --git a/src/lib/server/api/common/types/config.type.ts b/src/lib/server/api/common/types/config.type.ts new file mode 100644 index 0000000..7df400b --- /dev/null +++ b/src/lib/server/api/common/types/config.type.ts @@ -0,0 +1,26 @@ +export interface Config { + isProduction: boolean; + api: ApiConfig; + storage: StorageConfig; + redis: RedisConfig; + postgres: PostgresConfig; +} + +interface ApiConfig { + origin: string; +} + +interface StorageConfig { + accessKey: string; + secretKey: string; + bucket: string; + url: string; +} + +interface RedisConfig { + url: string; +} + +interface PostgresConfig { + url: string; +} \ No newline at end of file diff --git a/src/lib/server/api/common/classes/controller.class.ts b/src/lib/server/api/common/types/controller.ts similarity index 68% rename from src/lib/server/api/common/classes/controller.class.ts rename to src/lib/server/api/common/types/controller.ts index f6dfc5f..d1b26bf 100644 --- a/src/lib/server/api/common/classes/controller.class.ts +++ b/src/lib/server/api/common/types/controller.ts @@ -1,6 +1,6 @@ import { Hono } from "hono"; -import type { HonoTypes } from "../types/hono.type"; -import type { BlankSchema, Env, Schema } from "hono/types"; +import type { HonoTypes } from "./hono"; +import type { BlankSchema } from "hono/types"; export abstract class Controler { protected readonly controller: Hono; diff --git a/src/lib/server/api/common/inferfaces/email.interface.ts b/src/lib/server/api/common/types/email.ts similarity index 100% rename from src/lib/server/api/common/inferfaces/email.interface.ts rename to src/lib/server/api/common/types/email.ts diff --git a/src/lib/server/api/common/types/hono.type.ts b/src/lib/server/api/common/types/hono.ts similarity index 100% rename from src/lib/server/api/common/types/hono.type.ts rename to src/lib/server/api/common/types/hono.ts diff --git a/src/lib/server/api/common/inferfaces/mailer.interface.ts b/src/lib/server/api/common/types/mailer.ts similarity index 100% rename from src/lib/server/api/common/inferfaces/mailer.interface.ts rename to src/lib/server/api/common/types/mailer.ts diff --git a/src/lib/server/api/common/utils/repository.utils.ts b/src/lib/server/api/common/utils/repository.ts similarity index 100% rename from src/lib/server/api/common/utils/repository.utils.ts rename to src/lib/server/api/common/utils/repository.ts diff --git a/src/lib/server/api/common/utils/table.utils.ts b/src/lib/server/api/common/utils/table.ts similarity index 100% rename from src/lib/server/api/common/utils/table.utils.ts rename to src/lib/server/api/common/utils/table.ts diff --git a/src/lib/server/api/configs/envs.config.ts b/src/lib/server/api/configs/envs.config.ts deleted file mode 100644 index c17beeb..0000000 --- a/src/lib/server/api/configs/envs.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as envs from '$env/static/private'; - -export const env = { ...envs, isProduction: process.env.NODE_ENV === 'production' }; diff --git a/src/lib/server/api/controllers/iam.controller.ts b/src/lib/server/api/controllers/iam.controller.ts index 41ec692..58b3b39 100644 --- a/src/lib/server/api/controllers/iam.controller.ts +++ b/src/lib/server/api/controllers/iam.controller.ts @@ -1,22 +1,21 @@ -import { Hono, type Schema } from 'hono'; import { setCookie } from 'hono/cookie'; import { inject, injectable } from 'tsyringe'; import { zValidator } from '@hono/zod-validator'; import { IamService } from '../services/iam.service'; -import { LuciaProvider } from '../providers/lucia.provider'; import { limiter } from '../middlewares/rate-limiter.middlware'; import { requireAuth } from '../middlewares/auth.middleware'; -import { Controler } from '../common/classes/controller.class'; +import { Controler } from '../common/types/controller'; import { registerEmailDto } from '$lib/server/api/dtos/register-email.dto'; import { signInEmailDto } from '$lib/server/api/dtos/signin-email.dto'; import { updateEmailDto } from '$lib/server/api/dtos/update-email.dto'; import { verifyEmailDto } from '$lib/server/api/dtos/verify-email.dto'; +import { LuciaService } from '../services/lucia.service'; @injectable() -export class IamController extends Controler { +export class IamController extends Controler { constructor( @inject(IamService) private iamService: IamService, - @inject(LuciaProvider) private lucia: LuciaProvider, + @inject(LuciaService) private luciaService: LuciaService, ) { super(); } @@ -35,7 +34,7 @@ export class IamController extends Controler { .post('/login/verify', zValidator('json', signInEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => { const { email, token } = c.req.valid('json'); const session = await this.iamService.verifyLoginRequest({ email, token }); - const sessionCookie = this.lucia.createSessionCookie(session.id); + const sessionCookie = this.luciaService.lucia.createSessionCookie(session.id); setCookie(c, sessionCookie.name, sessionCookie.value, { path: sessionCookie.attributes.path, maxAge: sessionCookie.attributes.maxAge, @@ -50,7 +49,7 @@ export class IamController extends Controler { .post('/logout', requireAuth, async (c) => { const sessionId = c.var.session.id; await this.iamService.logout(sessionId); - const sessionCookie = this.lucia.createBlankSessionCookie(); + const sessionCookie = this.luciaService.lucia.createBlankSessionCookie(); setCookie(c, sessionCookie.name, sessionCookie.value, { path: sessionCookie.attributes.path, maxAge: sessionCookie.attributes.maxAge, diff --git a/src/lib/server/api/databases/migrations/0000_clear_paper_doll.sql b/src/lib/server/api/databases/postgres/migrations/0000_clear_paper_doll.sql similarity index 100% rename from src/lib/server/api/databases/migrations/0000_clear_paper_doll.sql rename to src/lib/server/api/databases/postgres/migrations/0000_clear_paper_doll.sql diff --git a/src/lib/server/api/databases/migrations/meta/0000_snapshot.json b/src/lib/server/api/databases/postgres/migrations/meta/0000_snapshot.json similarity index 100% rename from src/lib/server/api/databases/migrations/meta/0000_snapshot.json rename to src/lib/server/api/databases/postgres/migrations/meta/0000_snapshot.json diff --git a/src/lib/server/api/databases/migrations/meta/_journal.json b/src/lib/server/api/databases/postgres/migrations/meta/_journal.json similarity index 100% rename from src/lib/server/api/databases/migrations/meta/_journal.json rename to src/lib/server/api/databases/postgres/migrations/meta/_journal.json diff --git a/src/lib/server/api/databases/tables/email-verifications.table.ts b/src/lib/server/api/databases/postgres/tables/email-verifications.table.ts similarity index 93% rename from src/lib/server/api/databases/tables/email-verifications.table.ts rename to src/lib/server/api/databases/postgres/tables/email-verifications.table.ts index 8f6c4e1..03bffb6 100644 --- a/src/lib/server/api/databases/tables/email-verifications.table.ts +++ b/src/lib/server/api/databases/postgres/tables/email-verifications.table.ts @@ -2,7 +2,7 @@ import { createId } from '@paralleldrive/cuid2'; import { pgTable, text, timestamp } from 'drizzle-orm/pg-core'; import { relations } from 'drizzle-orm'; import { usersTable } from './users.table'; -import { timestamps } from '../../common/utils/table.utils'; +import { timestamps } from '../../../common/utils/table'; export const emailVerificationsTable = pgTable('email_verifications', { id: text('id') diff --git a/src/lib/server/api/databases/tables/files.table.ts b/src/lib/server/api/databases/postgres/tables/files.table.ts similarity index 84% rename from src/lib/server/api/databases/tables/files.table.ts rename to src/lib/server/api/databases/postgres/tables/files.table.ts index cc4c01b..1ac79f2 100644 --- a/src/lib/server/api/databases/tables/files.table.ts +++ b/src/lib/server/api/databases/postgres/tables/files.table.ts @@ -1,5 +1,5 @@ import { bigint, pgTable, text } from "drizzle-orm/pg-core"; -import { cuid2, timestamps } from "../../common/utils/table.utils"; +import { cuid2, timestamps } from "../../../common/utils/table"; import { createId } from "@paralleldrive/cuid2"; export const filesTable = pgTable('files', { diff --git a/src/lib/server/api/databases/tables/index.ts b/src/lib/server/api/databases/postgres/tables/index.ts similarity index 100% rename from src/lib/server/api/databases/tables/index.ts rename to src/lib/server/api/databases/postgres/tables/index.ts diff --git a/src/lib/server/api/databases/tables/login-requests.table.ts b/src/lib/server/api/databases/postgres/tables/login-requests.table.ts similarity index 90% rename from src/lib/server/api/databases/tables/login-requests.table.ts rename to src/lib/server/api/databases/postgres/tables/login-requests.table.ts index 820f15b..afb2a24 100644 --- a/src/lib/server/api/databases/tables/login-requests.table.ts +++ b/src/lib/server/api/databases/postgres/tables/login-requests.table.ts @@ -1,7 +1,7 @@ import { relations } from 'drizzle-orm'; import { createId } from '@paralleldrive/cuid2'; import { pgTable, text, timestamp } from 'drizzle-orm/pg-core'; -import { timestamps } from '../../common/utils/table.utils'; +import { timestamps } from '../../../common/utils/table'; export const loginRequestsTable = pgTable('login_requests', { id: text('id') diff --git a/src/lib/server/api/databases/tables/sessions.table.ts b/src/lib/server/api/databases/postgres/tables/sessions.table.ts similarity index 86% rename from src/lib/server/api/databases/tables/sessions.table.ts rename to src/lib/server/api/databases/postgres/tables/sessions.table.ts index 9df7e5c..dca266c 100644 --- a/src/lib/server/api/databases/tables/sessions.table.ts +++ b/src/lib/server/api/databases/postgres/tables/sessions.table.ts @@ -1,4 +1,4 @@ -import { cuid2 } from '../../common/utils/table.utils'; +import { cuid2 } from '../../../common/utils/table'; import { usersTable } from './users.table'; import { pgTable, text, timestamp } from 'drizzle-orm/pg-core'; diff --git a/src/lib/server/api/databases/tables/users.table.ts b/src/lib/server/api/databases/postgres/tables/users.table.ts similarity index 92% rename from src/lib/server/api/databases/tables/users.table.ts rename to src/lib/server/api/databases/postgres/tables/users.table.ts index 8506e39..956539e 100644 --- a/src/lib/server/api/databases/tables/users.table.ts +++ b/src/lib/server/api/databases/postgres/tables/users.table.ts @@ -3,7 +3,7 @@ import { createId } from '@paralleldrive/cuid2'; import { sessionsTable } from './sessions.table'; import { boolean, pgTable, text } from 'drizzle-orm/pg-core'; import { emailVerificationsTable } from './email-verifications.table'; -import { citext, cuid2, timestamps } from '../../common/utils/table.utils'; +import { citext, cuid2, timestamps } from '../../../common/utils/table'; import { filesTable } from './files.table'; diff --git a/src/lib/server/api/databases/redis/schemas/login-requests.schema.ts b/src/lib/server/api/databases/redis/schemas/login-requests.schema.ts new file mode 100644 index 0000000..f6c091a --- /dev/null +++ b/src/lib/server/api/databases/redis/schemas/login-requests.schema.ts @@ -0,0 +1,7 @@ +import { Schema } from 'redis-om' + +export const loginRequestSchema = new Schema('album', { + id: { type: 'string' }, + hashedToken: { type: 'string' }, + email: { type: 'string' }, +}) \ No newline at end of file diff --git a/src/lib/server/api/emails/email-change-notice.email.ts b/src/lib/server/api/emails/email-change-notice.email.ts index e1613d3..b096787 100644 --- a/src/lib/server/api/emails/email-change-notice.email.ts +++ b/src/lib/server/api/emails/email-change-notice.email.ts @@ -1,4 +1,4 @@ -import type { Email } from "../common/inferfaces/email.interface" +import type { Email } from "../common/types/email" export class EmailChangeNoticeEmail implements Email { constructor() { } diff --git a/src/lib/server/api/emails/login-verification.email.ts b/src/lib/server/api/emails/login-verification.email.ts index 26c0497..61a0e3c 100644 --- a/src/lib/server/api/emails/login-verification.email.ts +++ b/src/lib/server/api/emails/login-verification.email.ts @@ -1,4 +1,4 @@ -import type { Email } from "../common/inferfaces/email.interface" +import type { Email } from "../common/types/email" export class LoginVerificationEmail implements Email { constructor(private readonly token: string) { } diff --git a/src/lib/server/api/emails/welcome.email.ts b/src/lib/server/api/emails/welcome.email.ts index 233d594..ef54489 100644 --- a/src/lib/server/api/emails/welcome.email.ts +++ b/src/lib/server/api/emails/welcome.email.ts @@ -1,4 +1,4 @@ -import type { Email } from "../common/inferfaces/email.interface"; +import type { Email } from "../common/types/email"; export class WelcomeEmail implements Email { constructor() { } diff --git a/src/lib/server/api/index.ts b/src/lib/server/api/index.ts index c36ac4a..6260fb4 100644 --- a/src/lib/server/api/index.ts +++ b/src/lib/server/api/index.ts @@ -3,7 +3,7 @@ import { Hono } from 'hono'; import { hc } from 'hono/client'; import { container } from 'tsyringe'; import { IamController } from './controllers/iam.controller'; -import { env } from './configs/envs.config'; +import { config, env } from './common/config'; import { validateAuthSession, verifyOrigin } from './middlewares/auth.middleware'; import { AuthCleanupJobs } from './jobs/auth-cleanup.job'; @@ -32,7 +32,7 @@ container.resolve(AuthCleanupJobs).deleteStaleLoginRequests(); /* -------------------------------------------------------------------------- */ /* Exports */ /* -------------------------------------------------------------------------- */ -const rpc = hc(env.ORIGIN); +const rpc = hc(config.api.origin); export type ApiClient = typeof rpc; export type ApiRoutes = typeof routes; diff --git a/src/lib/server/api/jobs/auth-cleanup.job.ts b/src/lib/server/api/jobs/auth-cleanup.job.ts index 9202eaa..a841e13 100644 --- a/src/lib/server/api/jobs/auth-cleanup.job.ts +++ b/src/lib/server/api/jobs/auth-cleanup.job.ts @@ -8,10 +8,10 @@ export class AuthCleanupJobs { private queue; constructor(@inject(JobsService) private jobsService: JobsService) { - /* ------------------------------ Create Queue ------------------------------ */ + // create queue this.queue = this.jobsService.createQueue('auth_cleanup') - /* ---------------------------- Register Workers ---------------------------- */ + // register workers this.worker(); } diff --git a/src/lib/server/api/middlewares/auth.middleware.ts b/src/lib/server/api/middlewares/auth.middleware.ts index 04259ed..dc89240 100644 --- a/src/lib/server/api/middlewares/auth.middleware.ts +++ b/src/lib/server/api/middlewares/auth.middleware.ts @@ -3,9 +3,14 @@ import { createMiddleware } from 'hono/factory'; import { verifyRequestOrigin } from 'lucia'; import type { Session, User } from 'lucia'; import { Unauthorized } from '../common/exceptions'; -import type { HonoTypes } from '../common/types/hono.type'; -import { lucia } from '../packages/lucia'; +import type { HonoTypes } from '../common/types/hono'; +import { container } from 'tsyringe'; +import { LuciaService } from '../services/lucia.service'; +// resolve dependencies from the container +const { lucia } = container.resolve(LuciaService) + +// Middleware to verify the origin of the request export const verifyOrigin: MiddlewareHandler = createMiddleware(async (c, next) => { if (c.req.method === "GET") { return next(); diff --git a/src/lib/server/api/middlewares/rate-limiter.middlware.ts b/src/lib/server/api/middlewares/rate-limiter.middlware.ts index b5ee472..39f5841 100644 --- a/src/lib/server/api/middlewares/rate-limiter.middlware.ts +++ b/src/lib/server/api/middlewares/rate-limiter.middlware.ts @@ -1,11 +1,13 @@ import { rateLimiter } from "hono-rate-limiter"; import { RedisStore } from 'rate-limit-redis' -import RedisClient from 'ioredis' -import { env } from "../configs/envs.config"; -import type { HonoTypes } from "../common/types/hono.type"; +import type { HonoTypes } from "../common/types/hono"; +import { container } from "tsyringe"; +import { RedisService } from '../services/redis.service'; -const client = new RedisClient(env.REDIS_URL) +// resolve dependencies from the container +const { client } = container.resolve(RedisService); +// Rate limiter middleware export function limiter({ limit, minutes, key = "" }: { limit: number; minutes: number; @@ -23,8 +25,7 @@ export function limiter({ limit, minutes, key = "" }: { }, // 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), + sendCommand: (...args: string[]) => client.sendCommand(args), }) as any, }) } diff --git a/src/lib/server/api/packages/drizzle.ts b/src/lib/server/api/packages/drizzle.ts deleted file mode 100644 index 470076b..0000000 --- a/src/lib/server/api/packages/drizzle.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { drizzle } from 'drizzle-orm/postgres-js'; -import postgres from 'postgres'; -import * as schema from '../databases/tables'; -import { env } from '../configs/envs.config'; - -const client = postgres(env.DATABASE_URL!, { max: 1 }); -export const db = drizzle(client, { schema }); \ No newline at end of file diff --git a/src/lib/server/api/packages/lucia.ts b/src/lib/server/api/packages/lucia.ts deleted file mode 100644 index 8386e2f..0000000 --- a/src/lib/server/api/packages/lucia.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Lucia } from 'lucia'; -import { DrizzlePostgreSQLAdapter } from '@lucia-auth/adapter-drizzle'; -import { sessionsTable, usersTable } from '../databases/tables'; -import { env } from '../configs/envs.config'; -import { db } from './drizzle'; - -const adapter = new DrizzlePostgreSQLAdapter(db, sessionsTable, usersTable); - -export const lucia = new Lucia(adapter, { - sessionCookie: { - attributes: { - // set to `true` when using HTTPS - secure: env.isProduction - } - }, - getUserAttributes: (attributes) => { - return { - // attributes has the type of DatabaseUserAttributes - ...attributes - }; - } -}); diff --git a/src/lib/server/api/packages/s3.ts b/src/lib/server/api/packages/s3.ts deleted file mode 100644 index 98a8107..0000000 --- a/src/lib/server/api/packages/s3.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { S3Client } from '@aws-sdk/client-s3'; -import { env } from '$env/dynamic/private'; - -export const s3Client = new S3Client({ - region: 'auto', - endpoint: env.STORAGE_API_URL, - credentials: { - accessKeyId: env.STORAGE_API_ACCESS_KEY, - secretAccessKey: env.STORAGE_API_SECRET_KEY - }, - forcePathStyle: true -}) diff --git a/src/lib/server/api/providers/database.provider.ts b/src/lib/server/api/providers/database.provider.ts deleted file mode 100644 index 5912e44..0000000 --- a/src/lib/server/api/providers/database.provider.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { container } from 'tsyringe'; -import { db } from '../packages/drizzle'; - -export const DatabaseProvider = Symbol('DATABASE_TOKEN'); -export type DatabaseProvider = typeof db; -container.register(DatabaseProvider, { useValue: db }); diff --git a/src/lib/server/api/providers/lucia.provider.ts b/src/lib/server/api/providers/lucia.provider.ts deleted file mode 100644 index 408c724..0000000 --- a/src/lib/server/api/providers/lucia.provider.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { container } from 'tsyringe'; -import { lucia } from '../packages/lucia'; - -export const LuciaProvider = Symbol('LUCIA_PROVIDER'); -export type LuciaProvider = typeof 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 deleted file mode 100644 index 3653cfd..0000000 --- a/src/lib/server/api/providers/redis.provider.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { container } from 'tsyringe'; -import { env } from '../configs/envs.config'; -import RedisClient from 'ioredis' - -export const RedisProvider = Symbol('REDIS_TOKEN'); -export type RedisProvider = RedisClient; -container.register(RedisProvider, { - useValue: new RedisClient(env.REDIS_URL, { - maxRetriesPerRequest: null - }) -}); diff --git a/src/lib/server/api/providers/s3.provider.ts b/src/lib/server/api/providers/s3.provider.ts deleted file mode 100644 index 25c8508..0000000 --- a/src/lib/server/api/providers/s3.provider.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { container } from 'tsyringe'; -import { S3Client } from '@aws-sdk/client-s3'; -import { s3Client } from '../packages/s3'; - -export const S3ClientProvider = Symbol('STORAGE_TOKEN'); -export type S3ClientProvider = S3Client; -container.register(S3ClientProvider, { - useValue: s3Client -}); diff --git a/src/lib/server/api/repositories/email-verifications.repository.ts b/src/lib/server/api/repositories/email-verifications.repository.ts index 3a020f4..597ba3f 100644 --- a/src/lib/server/api/repositories/email-verifications.repository.ts +++ b/src/lib/server/api/repositories/email-verifications.repository.ts @@ -1,39 +1,35 @@ import { inject, injectable } from "tsyringe"; -import { and, eq, gte, lte, type InferInsertModel } from "drizzle-orm"; -import { emailVerificationsTable } from "../databases/tables"; -import type { Repository } from "../common/inferfaces/repository.interface"; -import { DatabaseProvider } from "../providers/database.provider"; -import { takeFirst, takeFirstOrThrow } from "../common/utils/repository.utils"; - +import { and, eq, gte, type InferInsertModel } from "drizzle-orm"; +import { emailVerificationsTable } from "../databases/postgres/tables"; +import { takeFirst, takeFirstOrThrow } from "../common/utils/repository"; +import { DrizzleService } from "../services/drizzle.service"; export type CreateEmailVerification = Pick, 'requestedEmail' | 'hashedToken' | 'userId' | 'expiresAt'>; @injectable() -export class EmailVerificationsRepository implements Repository { - constructor(@inject(DatabaseProvider) private readonly db: DatabaseProvider) { } +export class EmailVerificationsRepository { + constructor(@inject(DrizzleService) private readonly drizzle: DrizzleService) { } // creates a new email verification record or updates an existing one async create(data: CreateEmailVerification) { - return this.db.insert(emailVerificationsTable).values(data).onConflictDoUpdate({ + return this.drizzle.db.insert(emailVerificationsTable).values(data).onConflictDoUpdate({ target: emailVerificationsTable.userId, set: data }).returning().then(takeFirstOrThrow) } // finds a valid record by token and userId - async findValidRecord(userId: string) { - return this.db.select().from(emailVerificationsTable).where( + async findValidRecord(userId: string, db = this.drizzle.db) { + return db.select().from(emailVerificationsTable).where( and( eq(emailVerificationsTable.userId, userId), gte(emailVerificationsTable.expiresAt, new Date()) )).then(takeFirst) } - async deleteById(id: string) { - return this.db.delete(emailVerificationsTable).where(eq(emailVerificationsTable.id, id)) + async deleteById(id: string, db = this.drizzle.db) { + return db.delete(emailVerificationsTable).where(eq(emailVerificationsTable.id, id)) } - trxHost(trx: DatabaseProvider) { - return new EmailVerificationsRepository(trx) - } + } \ No newline at end of file diff --git a/src/lib/server/api/repositories/files.repository.ts b/src/lib/server/api/repositories/files.repository.ts index 7ba81ae..2208145 100644 --- a/src/lib/server/api/repositories/files.repository.ts +++ b/src/lib/server/api/repositories/files.repository.ts @@ -1,29 +1,29 @@ import { inject } from "tsyringe"; import { StorageService } from "../services/storage.service"; -import { DatabaseProvider } from "../providers/database.provider"; import { eq } from "drizzle-orm"; -import { filesTable } from "../databases/tables/files.table"; -import { takeFirst, takeFirstOrThrow } from "../common/utils/repository.utils"; +import { filesTable } from "../databases/postgres/tables/files.table"; +import { takeFirst, takeFirstOrThrow } from "../common/utils/repository"; +import { DrizzleService } from "../services/drizzle.service"; export class FilesRepository { constructor( @inject(StorageService) private readonly storageService: StorageService, - @inject(DatabaseProvider) private readonly db: DatabaseProvider) { } + @inject(DrizzleService) private readonly drizzle: DrizzleService) { } - async create(file: File, db = this.db) { + async create(file: File, db = this.drizzle.db) { const asset = await this.storageService.upload(file); return db.insert(filesTable).values({ key: asset.key, contentType: asset.type, size: BigInt(asset.size) }).returning().then(takeFirst) } - async findOneById(id: string, db = this.db) { + async findOneById(id: string, db = this.drizzle.db) { return db.select().from(filesTable).where(eq(filesTable.id, id)).then(takeFirst) } - async findOneByIdOrThrow(id: string, db = this.db) { + async findOneByIdOrThrow(id: string, db = this.drizzle.db) { return db.select().from(filesTable).where(eq(filesTable.id, id)).then(takeFirstOrThrow) } - async update(id: string, file: File, db = this.db) { + async update(id: string, file: File, db = this.drizzle.db) { // upload new file const newAsset = await this.storageService.upload(file); await db.update(filesTable).set({ key: newAsset.key, contentType: newAsset.type, size: BigInt(newAsset.size) }).where(eq(filesTable.id, id)) @@ -33,7 +33,7 @@ export class FilesRepository { await this.storageService.delete(oldAsset.key) } - async delete(id: string, db = this.db) { + async delete(id: string, db = this.drizzle.db) { const asset = await this.findOneByIdOrThrow(id) await this.storageService.delete(asset.key) await db.delete(filesTable).where(eq(filesTable.id, id)) diff --git a/src/lib/server/api/repositories/login-requests.repository.ts b/src/lib/server/api/repositories/login-requests.repository.ts index 86587c0..141566c 100644 --- a/src/lib/server/api/repositories/login-requests.repository.ts +++ b/src/lib/server/api/repositories/login-requests.repository.ts @@ -1,26 +1,27 @@ import { inject, injectable } from "tsyringe"; import { and, eq, gte, type InferInsertModel } from "drizzle-orm"; -import { loginRequestsTable } from "../databases/tables"; -import type { Repository } from "../common/inferfaces/repository.interface"; -import { DatabaseProvider } from "../providers/database.provider"; -import { takeFirst, takeFirstOrThrow } from "../common/utils/repository.utils"; +import { loginRequestsTable } from "../databases/postgres/tables"; +import { takeFirst, takeFirstOrThrow } from "../common/utils/repository"; +import { DrizzleService } from "../services/drizzle.service"; export type CreateLoginRequest = Pick, 'email' | 'expiresAt' | 'hashedToken'>; @injectable() -export class LoginRequestsRepository implements Repository { - constructor(@inject(DatabaseProvider) private readonly db: DatabaseProvider) { } +export class LoginRequestsRepository { + constructor( + @inject(DrizzleService) private readonly drizzle: DrizzleService, + ) { } - async create(data: CreateLoginRequest) { - return this.db.insert(loginRequestsTable).values(data).onConflictDoUpdate({ + async create(data: CreateLoginRequest, db = this.drizzle.db) { + return db.insert(loginRequestsTable).values(data).onConflictDoUpdate({ target: loginRequestsTable.email, set: data }).returning().then(takeFirstOrThrow) } - async findOneByEmail(email: string) { - return this.db.select().from(loginRequestsTable).where( + async findOneByEmail(email: string, db = this.drizzle.db) { + return db.select().from(loginRequestsTable).where( and( eq(loginRequestsTable.email, email), gte(loginRequestsTable.expiresAt, new Date()) @@ -28,11 +29,7 @@ export class LoginRequestsRepository implements Repository { ).then(takeFirst) } - async deleteById(id: string) { - return this.db.delete(loginRequestsTable).where(eq(loginRequestsTable.id, id)); - } - - trxHost(trx: DatabaseProvider) { - return new LoginRequestsRepository(trx); + async deleteById(id: string, db = this.drizzle.db) { + return db.delete(loginRequestsTable).where(eq(loginRequestsTable.id, id)); } } \ No newline at end of file diff --git a/src/lib/server/api/repositories/users.repository.ts b/src/lib/server/api/repositories/users.repository.ts index 0ca5ccb..aafe9dd 100644 --- a/src/lib/server/api/repositories/users.repository.ts +++ b/src/lib/server/api/repositories/users.repository.ts @@ -1,49 +1,45 @@ import { inject, injectable } from 'tsyringe'; -import { usersTable } from '../databases/tables'; +import { usersTable } from '../databases/postgres/tables'; import { eq, type InferInsertModel } from 'drizzle-orm'; -import { DatabaseProvider } from '../providers/database.provider'; -import { takeFirstOrThrow } from '../common/utils/repository.utils'; +import { takeFirstOrThrow } from '../common/utils/repository'; import type { Repository } from '../common/inferfaces/repository.interface'; +import { DrizzleService } from '../services/drizzle.service'; export type CreateUser = InferInsertModel; export type UpdateUser = Partial; @injectable() -export class UsersRepository implements Repository { - constructor(@inject(DatabaseProvider) private db: DatabaseProvider) { } +export class UsersRepository { + constructor(@inject(DrizzleService) private drizzle: DrizzleService) { } - async findOneById(id: string) { - return this.db.query.usersTable.findFirst({ + async findOneById(id: string, db = this.drizzle.db) { + return db.query.usersTable.findFirst({ where: eq(usersTable.id, id) }); } - async findOneByIdOrThrow(id: string) { - const user = await this.findOneById(id); + async findOneByIdOrThrow(id: string, db = this.drizzle.db) { + const user = await this.findOneById(id, db); if (!user) throw Error('User not found'); return user; } - async findOneByEmail(email: string) { - return this.db.query.usersTable.findFirst({ + async findOneByEmail(email: string, db = this.drizzle.db) { + return db.query.usersTable.findFirst({ where: eq(usersTable.email, email) }); } - async create(data: CreateUser) { - return this.db.insert(usersTable).values(data).returning().then(takeFirstOrThrow); + async create(data: CreateUser, db = this.drizzle.db) { + return db.insert(usersTable).values(data).returning().then(takeFirstOrThrow); } - async update(id: string, data: UpdateUser) { - return this.db + async update(id: string, data: UpdateUser, db = this.drizzle.db) { + return db .update(usersTable) .set(data) .where(eq(usersTable.id, id)) .returning() .then(takeFirstOrThrow); } - - trxHost(trx: DatabaseProvider) { - return new UsersRepository(trx); - } } diff --git a/src/lib/server/api/services/drizzle.service.ts b/src/lib/server/api/services/drizzle.service.ts new file mode 100644 index 0000000..3ee4fa2 --- /dev/null +++ b/src/lib/server/api/services/drizzle.service.ts @@ -0,0 +1,22 @@ +import { drizzle, type PostgresJsDatabase } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; +import { injectable, type Disposable } from "tsyringe"; +import { config } from "../common/config"; +import * as schema from '../databases/postgres/tables'; + +@injectable() +export class DrizzleService implements Disposable { + protected readonly client: postgres.Sql<{}> + readonly db: PostgresJsDatabase + readonly schema: typeof schema = schema; + + constructor() { + const client = postgres(config.postgres.url, { max: 1 }) + this.client = client; + this.db = drizzle(client, { schema }) + } + + dispose(): Promise | void { + this.client.end(); + } +} \ No newline at end of file diff --git a/src/lib/server/api/services/iam.service.ts b/src/lib/server/api/services/iam.service.ts index 35bce7d..c78b340 100644 --- a/src/lib/server/api/services/iam.service.ts +++ b/src/lib/server/api/services/iam.service.ts @@ -1,23 +1,23 @@ import { inject, injectable } from 'tsyringe'; import { MailerService } from './mailer.service'; import { TokensService } from './tokens.service'; -import { LuciaProvider } from '../providers/lucia.provider'; import { UsersRepository } from '../repositories/users.repository'; import type { SignInEmailDto } from '../dtos/signin-email.dto'; import type { RegisterEmailDto } from '../dtos/register-email.dto'; import { LoginRequestsRepository } from '../repositories/login-requests.repository'; import { LoginVerificationEmail } from '../emails/login-verification.email'; -import { DatabaseProvider } from '../providers/database.provider'; import { BadRequest } from '../common/exceptions'; import { WelcomeEmail } from '../emails/welcome.email'; import { EmailVerificationsRepository } from '../repositories/email-verifications.repository'; import { EmailChangeNoticeEmail } from '../emails/email-change-notice.email'; +import { DrizzleService } from './drizzle.service'; +import { LuciaService } from './lucia.service'; @injectable() export class IamService { constructor( - @inject(LuciaProvider) private readonly lucia: LuciaProvider, - @inject(DatabaseProvider) private readonly db: DatabaseProvider, + @inject(LuciaService) private readonly luciaService: LuciaService, + @inject(DrizzleService) private readonly drizzleService: DrizzleService, @inject(TokensService) private readonly tokensService: TokensService, @inject(MailerService) private readonly mailerService: MailerService, @inject(UsersRepository) private readonly usersRepository: UsersRepository, @@ -42,10 +42,10 @@ export class IamService { if (!existingUser) { const newUser = await this.handleNewUserRegistration(data.email); - return this.lucia.createSession(newUser.id, {}); + return this.luciaService.lucia.createSession(newUser.id, {}); } - return this.lucia.createSession(existingUser.id, {}); + return this.luciaService.lucia.createSession(existingUser.id, {}); } // These steps follow the process outlined in OWASP's "Changing A User's Email Address" guide. @@ -80,7 +80,7 @@ export class IamService { } async logout(sessionId: string) { - return this.lucia.invalidateSession(sessionId); + return this.luciaService.lucia.invalidateSession(sessionId); } // Create a new user and send a welcome email - or other onboarding process @@ -93,9 +93,9 @@ export class IamService { // Fetch a valid request from the database, verify the token and burn the request if it is valid private async getValidLoginRequest(email: string, token: string) { - return await this.db.transaction(async (trx) => { + return await this.drizzleService.db.transaction(async (trx) => { // fetch the login request - const loginRequest = await this.loginRequestsRepository.trxHost(trx).findOneByEmail(email) + const loginRequest = await this.loginRequestsRepository.findOneByEmail(email, trx) if (!loginRequest) return null; // check if the token is valid @@ -103,15 +103,15 @@ export class IamService { if (!isValidRequest) return null // if the token is valid, burn the request - await this.loginRequestsRepository.trxHost(trx).deleteById(loginRequest.id); + await this.loginRequestsRepository.deleteById(loginRequest.id, trx); return loginRequest }) } private async findAndBurnEmailVerificationToken(userId: string, token: string) { - return this.db.transaction(async (trx) => { + return this.drizzleService.db.transaction(async (trx) => { // find a valid record - const emailVerificationRecord = await this.emailVerificationsRepository.trxHost(trx).findValidRecord(userId); + const emailVerificationRecord = await this.emailVerificationsRepository.findValidRecord(userId, trx); if (!emailVerificationRecord) return null; // check if the token is valid @@ -119,7 +119,7 @@ export class IamService { if (!isValidRecord) return null // burn the token if it is valid - await this.emailVerificationsRepository.trxHost(trx).deleteById(emailVerificationRecord.id) + await this.emailVerificationsRepository.deleteById(emailVerificationRecord.id, trx) return emailVerificationRecord }) } diff --git a/src/lib/server/api/services/jobs.service.ts b/src/lib/server/api/services/jobs.service.ts index 3bd5f55..5545233 100644 --- a/src/lib/server/api/services/jobs.service.ts +++ b/src/lib/server/api/services/jobs.service.ts @@ -1,16 +1,28 @@ -import { inject, injectable } from "tsyringe"; +import { injectable } from "tsyringe"; import { Queue, Worker, type Processor } from 'bullmq'; -import { RedisProvider } from "../providers/redis.provider"; +import RedisClient from "ioredis"; +import { config } from "../common/config"; + +// BullMQ utilizes ioredis, which is no longer maintained but still works fine. +// I recommend using BullMQ with ioredis for now, but keep an eye out for future updates. @injectable() export class JobsService { - constructor(@inject(RedisProvider) private readonly redis: RedisProvider) { } + constructor() { } createQueue(name: string) { - return new Queue(name, { connection: this.redis }) + return new Queue(name, { + connection: new RedisClient(config.redis.url, { + maxRetriesPerRequest: null + }) + }) } createWorker(name: string, prcoessor: Processor) { - return new Worker(name, prcoessor, { connection: this.redis }) + return new Worker(name, prcoessor, { + connection: new RedisClient(config.redis.url, { + maxRetriesPerRequest: null + }) + }) } } diff --git a/src/lib/server/api/services/lucia.service.ts b/src/lib/server/api/services/lucia.service.ts new file mode 100644 index 0000000..7c6355e --- /dev/null +++ b/src/lib/server/api/services/lucia.service.ts @@ -0,0 +1,25 @@ +import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle"; +import { Lucia } from "lucia"; +import { inject, injectable } from "tsyringe"; +import { DrizzleService } from "./drizzle.service"; +import { config } from "../common/config"; + +@injectable() +export class LuciaService { + readonly lucia: Lucia; + constructor(@inject(DrizzleService) private readonly drizzle: DrizzleService) { + const adapter = new DrizzlePostgreSQLAdapter(this.drizzle.db, this.drizzle.schema.sessionsTable, this.drizzle.schema.usersTable); + this.lucia = new Lucia(adapter, { + sessionCookie: { + attributes: { + secure: config.isProduction + } + }, + getUserAttributes: (attributes) => { + return { + ...attributes + }; + } + }); + } +} \ No newline at end of file diff --git a/src/lib/server/api/services/mailer.service.ts b/src/lib/server/api/services/mailer.service.ts index 9b78041..d805fca 100644 --- a/src/lib/server/api/services/mailer.service.ts +++ b/src/lib/server/api/services/mailer.service.ts @@ -1,6 +1,6 @@ import { injectable } from 'tsyringe'; -import { env } from '../configs/envs.config'; import type { Email } from '../common/inferfaces/email.interface'; +import { config } from '../common/config'; type SendProps = { to: string | string[]; @@ -11,7 +11,7 @@ type SendProps = { export class MailerService { async send(data: SendProps) { - const mailer = env.isProduction ? this.sendProd : this.sendDev; + const mailer = config.isProduction ? this.sendProd : this.sendDev; await mailer(data); } diff --git a/src/lib/server/api/services/redis.service.ts b/src/lib/server/api/services/redis.service.ts new file mode 100644 index 0000000..9d6fb02 --- /dev/null +++ b/src/lib/server/api/services/redis.service.ts @@ -0,0 +1,42 @@ +import { createClient, type RedisClientType } from "redis"; +import { injectable, type Disposable } from "tsyringe"; +import { config } from "../common/config"; +import type { AsyncService } from "../common/inferfaces/async-service.interface"; + +@injectable() +export class RedisService implements Disposable, AsyncService { + readonly client: RedisClientType; + private isConnected: boolean = false; + + constructor() { + this.client = createClient({ + url: config.redis.url, + }); + this.init(); + } + + async ensureConnected(): Promise { + if (!this.isConnected) { + await this.init(); + } + } + + async init(): Promise { + try { + await this.client.connect(); + this.isConnected = this.client.isReady; + console.log('Redis connected'); + } catch (error) { + console.error('Failed to connect to Redis:', error); + throw error; + } + } + + async dispose(): Promise { + if (this.isConnected) { + await this.client.disconnect(); + this.isConnected = false; + console.log('Redis disconnected'); + } + } +} \ No newline at end of file diff --git a/src/lib/server/api/services/storage.service.ts b/src/lib/server/api/services/storage.service.ts index 93fbce4..a15c967 100644 --- a/src/lib/server/api/services/storage.service.ts +++ b/src/lib/server/api/services/storage.service.ts @@ -1,17 +1,28 @@ -import { PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3'; +import { PutObjectCommand, DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3'; import { createId } from '@paralleldrive/cuid2'; -import { inject, injectable } from 'tsyringe'; -import { env } from '../configs/envs.config'; -import { S3ClientProvider } from '../providers/s3.provider'; +import { injectable } from 'tsyringe'; +import { config } from '../common/config'; @injectable() export class StorageService { - constructor(@inject(S3ClientProvider) private readonly s3Client: S3ClientProvider) { } + protected readonly s3Client: S3Client + + constructor() { + this.s3Client = new S3Client({ + region: 'auto', + endpoint: config.storage.url, + credentials: { + accessKeyId: config.storage.accessKey, + secretAccessKey: config.storage.secretKey + }, + forcePathStyle: true + }) + } async upload(file: File) { const key = createId(); const uploadCommand = new PutObjectCommand({ - Bucket: env.STORAGE_BUCKET_NAME, + Bucket: config.storage.bucket, ACL: 'public-read', Key: key, ContentType: file.type, @@ -24,7 +35,7 @@ export class StorageService { delete(key: string) { const deleteCommand = new DeleteObjectCommand({ - Bucket: env.STORAGE_BUCKET_NAME, + Bucket: config.storage.bucket, Key: key }); diff --git a/src/lib/server/api/tests/login-requests.service.test.ts b/src/lib/server/api/tests/login-requests.service.test.ts index a532b85..44e54b6 100644 --- a/src/lib/server/api/tests/login-requests.service.test.ts +++ b/src/lib/server/api/tests/login-requests.service.test.ts @@ -1,23 +1,22 @@ import 'reflect-metadata'; -import { LoginRequestsService } from '../services/login-requests.service'; import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; import { TokensService } from '../services/tokens.service'; import { MailerService } from '../services/mailer.service'; import { UsersRepository } from '../repositories/users.repository'; import { LoginRequestsRepository } from '../repositories/login-requests.repository'; -import { PgDatabase } from 'drizzle-orm/pg-core'; import { container } from 'tsyringe'; -import { LuciaProvider } from '../providers/lucia.provider'; -import { DatabaseProvider } from '../providers/database.provider'; +import { LuciaService } from '../services/lucia.service'; +import { DrizzleService } from '../services/drizzle.service'; +import { IamService } from '../services/iam.service'; describe('LoginRequestService', () => { - let service: LoginRequestsService; + let service: IamService; let tokensService = vi.mocked(TokensService.prototype) let mailerService = vi.mocked(MailerService.prototype); let usersRepository = vi.mocked(UsersRepository.prototype); let loginRequestsRepository = vi.mocked(LoginRequestsRepository.prototype); - let luciaProvider = vi.mocked(LuciaProvider); - let databaseProvider = vi.mocked(PgDatabase); + let luciaService = vi.mocked(LuciaService.prototype); + let drizzleService = vi.mocked(DrizzleService.prototype); beforeAll(() => { service = container @@ -25,9 +24,9 @@ describe('LoginRequestService', () => { .register(MailerService, { useValue: mailerService }) .register(UsersRepository, { useValue: usersRepository }) .register(LoginRequestsRepository, { useValue: loginRequestsRepository }) - .register(LuciaProvider, { useValue: luciaProvider }) - .register(DatabaseProvider, { useValue: databaseProvider }) - .resolve(LoginRequestsService); + .register(LuciaService, { useValue: luciaService }) + .register(DrizzleService, { useValue: drizzleService }) + .resolve(IamService); }); afterAll(() => { @@ -57,7 +56,7 @@ describe('LoginRequestService', () => { const spy_loginRequestsRepository_create = vi.spyOn(loginRequestsRepository, 'create') it('should resolve', async () => { - await expect(service.create({ email: "test" })).resolves.toBeUndefined() + await expect(service.createLoginRequest({ email: "test" })).resolves.toBeUndefined() }) it('should generate a token with expiry and hash', async () => { expect(spy_tokensService_generateTokenWithExpiryAndHash).toBeCalledTimes(1) diff --git a/src/routes/(auth)/register/+page.server.ts b/src/routes/(auth)/register/+page.server.ts index e7ff0a0..de2ef34 100644 --- a/src/routes/(auth)/register/+page.server.ts +++ b/src/routes/(auth)/register/+page.server.ts @@ -7,7 +7,7 @@ import { registerFormSchema, signInFormSchema } from './schemas'; export const load = async () => { return { emailRegisterForm: await superValidate(zod(registerFormSchema)), - emailSigninForm: await superValidate(zod(signInFormSchema)) + emailSigninForm: await superValidate({email: 'test'}, zod(signInFormSchema)) }; };