From 117fac66b363cfe956f0ec2368e59a493c32500e Mon Sep 17 00:00:00 2001 From: Mariusz Wachowski Date: Thu, 17 Oct 2024 21:57:41 +0200 Subject: [PATCH 1/3] replaced iam service with authentication service --- README.md | 2 +- src/hooks.server.ts | 4 +- src/lib/server/api/common/types/controller.ts | 2 + ...roller.ts => authentication.controller.ts} | 2 +- src/lib/server/api/index.ts | 9 +-- .../api/services/authentication.service.ts | 4 - src/lib/server/api/services/iam.service.ts | 80 ------------------- .../api/tests/login-requests.service.test.ts | 6 +- src/routes/(app)/+page.server.ts | 2 +- .../(app)/settings/account/+page.server.ts | 4 +- src/routes/(auth)/register/+page.server.ts | 4 +- 11 files changed, 16 insertions(+), 103 deletions(-) rename src/lib/server/api/controllers/{iam.controller.ts => authentication.controller.ts} (98%) delete mode 100644 src/lib/server/api/services/iam.service.ts diff --git a/README.md b/README.md index 274d0fd..114726a 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ to start with Technical and let the project naturally evolve into one of the oth ### File Naming -You might notice how each file in the backend is postfixed with its architectural type(e.g. `iam.service.ts`). This allows +You might notice how each file in the backend is postfixed with its architectural type(e.g. `authentication.service.ts`). This allows us to easily reorganize the folder structure to suite a different architecture pattern if the domain becomes more complex. For example, if you want to group folders by domain(DDD), you simply drag and drop all related files to that folder. diff --git a/src/hooks.server.ts b/src/hooks.server.ts index c0de2e1..08ad973 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -18,12 +18,12 @@ const apiClient: Handle = async ({ event, resolve }) => { /* ----------------------------- Auth functions ----------------------------- */ async function getAuthedUser() { - const { data } = await api.iam.me.$get().then(parseApiResponse) + const { data } = await api.auth.me.$get().then(parseApiResponse) return data && data.user; } async function getAuthedUserOrThrow(redirectTo = '/') { - const { data } = await api.iam.me.$get().then(parseApiResponse); + const { data } = await api.auth.me.$get().then(parseApiResponse); if (!data || !data.user) throw redirect(StatusCodes.TEMPORARY_REDIRECT, redirectTo); return data?.user; } diff --git a/src/lib/server/api/common/types/controller.ts b/src/lib/server/api/common/types/controller.ts index d1b26bf..fa3e01e 100644 --- a/src/lib/server/api/common/types/controller.ts +++ b/src/lib/server/api/common/types/controller.ts @@ -4,8 +4,10 @@ import type { BlankSchema } from "hono/types"; export abstract class Controler { protected readonly controller: Hono; + constructor() { this.controller = new Hono(); } + abstract routes(): Hono; } \ No newline at end of file diff --git a/src/lib/server/api/controllers/iam.controller.ts b/src/lib/server/api/controllers/authentication.controller.ts similarity index 98% rename from src/lib/server/api/controllers/iam.controller.ts rename to src/lib/server/api/controllers/authentication.controller.ts index 4b45dac..be85531 100644 --- a/src/lib/server/api/controllers/iam.controller.ts +++ b/src/lib/server/api/controllers/authentication.controller.ts @@ -13,7 +13,7 @@ import { loginDto } from '../dtos/login.dto'; import { verifyLoginDto } from '../dtos/verify-login.dto'; @injectable() -export class IamController extends Controler { +export class AuthenticationController extends Controler { constructor( @inject(AuthenticationService) private authenticationService: AuthenticationService, @inject(EmailVerificationService) private emailVerificationService: EmailVerificationService, diff --git a/src/lib/server/api/index.ts b/src/lib/server/api/index.ts index 4260474..25a23a0 100644 --- a/src/lib/server/api/index.ts +++ b/src/lib/server/api/index.ts @@ -2,7 +2,7 @@ import 'reflect-metadata'; import { Hono } from 'hono'; import { hc } from 'hono/client'; import { container } from 'tsyringe'; -import { IamController } from './controllers/iam.controller'; +import { AuthenticationController } from './controllers/authentication.controller'; import { config } from './common/config'; import { validateAuthSession, verifyOrigin } from './middlewares/auth.middleware'; import { AuthCleanupJobs } from './jobs/auth-cleanup.job'; @@ -20,8 +20,7 @@ app.use(verifyOrigin).use(validateAuthSession); /* -------------------------------------------------------------------------- */ /* Routes */ /* -------------------------------------------------------------------------- */ -const routes = app - .route('/iam', container.resolve(IamController).routes()) +const routes = app.route('/auth', container.resolve(AuthenticationController).routes()); /* -------------------------------------------------------------------------- */ /* Cron Jobs */ @@ -35,7 +34,3 @@ container.resolve(AuthCleanupJobs).deleteStaleLoginRequests(); const rpc = hc(config.api.origin); export type ApiClient = typeof rpc; export type ApiRoutes = typeof routes; - - - - diff --git a/src/lib/server/api/services/authentication.service.ts b/src/lib/server/api/services/authentication.service.ts index 5f09507..53ac572 100644 --- a/src/lib/server/api/services/authentication.service.ts +++ b/src/lib/server/api/services/authentication.service.ts @@ -73,8 +73,4 @@ export class AuthenticationService { return loginRequest }) } - - - - } diff --git a/src/lib/server/api/services/iam.service.ts b/src/lib/server/api/services/iam.service.ts deleted file mode 100644 index eb6ea11..0000000 --- a/src/lib/server/api/services/iam.service.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { inject, injectable } from 'tsyringe'; -import { MailerService } from './mailer.service'; -import { TokensService } from './tokens.service'; -import { UsersRepository } from '../repositories/users.repository'; -import type { VerifyLoginDto } from '../dtos/verify-login.dto'; -import type { LoginDto } from '../dtos/login.dto'; -import { LoginRequestsRepository } from '../repositories/login-requests.repository'; -import { LoginVerificationEmail } from '../emails/login-verification.email'; -import { BadRequest } from '../common/exceptions'; -import { WelcomeEmail } from '../emails/welcome.email'; -import { DrizzleService } from './drizzle.service'; -import { LuciaService } from './lucia.service'; - -@injectable() -export class IamService { - constructor( - @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, - @inject(LoginRequestsRepository) private readonly loginRequestsRepository: LoginRequestsRepository, - ) { } - - async createLoginRequest(data: LoginDto) { - // 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.send({ email: new LoginVerificationEmail(token), to: data.email }); - } - - async verifyLoginRequest(data: VerifyLoginDto) { - const validLoginRequest = await this.getValidLoginRequest(data.email, data.token); - if (!validLoginRequest) throw BadRequest('Invalid token'); - - let existingUser = await this.usersRepository.findOneByEmail(data.email); - - if (!existingUser) { - const newUser = await this.handleNewUserRegistration(data.email); - return this.luciaService.lucia.createSession(newUser.id, {}); - } - - return this.luciaService.lucia.createSession(existingUser.id, {}); - } - - async logout(sessionId: string) { - return this.luciaService.lucia.invalidateSession(sessionId); - } - - // 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 }) - await this.mailerService.send({ email: new WelcomeEmail(), to: newUser.email }); - // 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 getValidLoginRequest(email: string, token: string) { - return await this.drizzleService.db.transaction(async (trx) => { - // fetch the login request - const loginRequest = await this.loginRequestsRepository.findOneByEmail(email, trx) - if (!loginRequest) 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.deleteById(loginRequest.id, trx); - return loginRequest - }) - } - - - - -} 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 44e54b6..d8ece68 100644 --- a/src/lib/server/api/tests/login-requests.service.test.ts +++ b/src/lib/server/api/tests/login-requests.service.test.ts @@ -7,10 +7,10 @@ import { LoginRequestsRepository } from '../repositories/login-requests.reposito import { container } from 'tsyringe'; import { LuciaService } from '../services/lucia.service'; import { DrizzleService } from '../services/drizzle.service'; -import { IamService } from '../services/iam.service'; +import { AuthenticationService } from '../services/authentication.service'; describe('LoginRequestService', () => { - let service: IamService; + let service: AuthenticationService; let tokensService = vi.mocked(TokensService.prototype) let mailerService = vi.mocked(MailerService.prototype); let usersRepository = vi.mocked(UsersRepository.prototype); @@ -26,7 +26,7 @@ describe('LoginRequestService', () => { .register(LoginRequestsRepository, { useValue: loginRequestsRepository }) .register(LuciaService, { useValue: luciaService }) .register(DrizzleService, { useValue: drizzleService }) - .resolve(IamService); + .resolve(AuthenticationService); }); afterAll(() => { diff --git a/src/routes/(app)/+page.server.ts b/src/routes/(app)/+page.server.ts index b0477fd..869147e 100644 --- a/src/routes/(app)/+page.server.ts +++ b/src/routes/(app)/+page.server.ts @@ -8,7 +8,7 @@ export const load = async ({ locals }) => { export const actions = { logout: async ({ locals }) => { - await locals.api.iam.logout.$post() + await locals.api.auth.logout.$post() redirect(StatusCodes.SEE_OTHER, '/register') } } diff --git a/src/routes/(app)/settings/account/+page.server.ts b/src/routes/(app)/settings/account/+page.server.ts index 31f55f1..d91f350 100644 --- a/src/routes/(app)/settings/account/+page.server.ts +++ b/src/routes/(app)/settings/account/+page.server.ts @@ -17,7 +17,7 @@ export const actions = { updateEmail: async ({ request, locals }) => { const updateEmailForm = await superValidate(request, zod(updateEmailFormSchema)); if (!updateEmailForm.valid) return fail(StatusCodes.BAD_REQUEST, { updateEmailForm }) - const { error } = await locals.api.iam.email.$patch({ json: updateEmailForm.data }).then(locals.parseApiResponse); + const { error } = await locals.api.auth.email.$patch({ json: updateEmailForm.data }).then(locals.parseApiResponse); if (error) return setError(updateEmailForm, 'email', error); return { updateEmailForm } }, @@ -25,7 +25,7 @@ export const actions = { const verifyEmailForm = await superValidate(request, zod(verifyEmailFormSchema)); console.log(verifyEmailForm) if (!verifyEmailForm.valid) return fail(StatusCodes.BAD_REQUEST, { verifyEmailForm }) - const { error } = await locals.api.iam.email.verification.$post({ json: verifyEmailForm.data }).then(locals.parseApiResponse); + const { error } = await locals.api.auth.email.verification.$post({ json: verifyEmailForm.data }).then(locals.parseApiResponse); if (error) return setError(verifyEmailForm, 'token', error); return { verifyEmailForm } } diff --git a/src/routes/(auth)/register/+page.server.ts b/src/routes/(auth)/register/+page.server.ts index a0612a8..44fb6fe 100644 --- a/src/routes/(auth)/register/+page.server.ts +++ b/src/routes/(auth)/register/+page.server.ts @@ -15,14 +15,14 @@ export const actions = { register: async ({ locals, request }) => { const emailRegisterForm = await superValidate(request, zod(registerFormSchema)); if (!emailRegisterForm.valid) return fail(StatusCodes.BAD_REQUEST, { emailRegisterForm }); - const { error } = await locals.api.iam.login.$post({ json: emailRegisterForm.data }).then(locals.parseApiResponse); + const { error } = await locals.api.auth.login.$post({ json: emailRegisterForm.data }).then(locals.parseApiResponse); if (error) return setError(emailRegisterForm, 'email', error); return { emailRegisterForm }; }, signin: async ({ locals, request }) => { const emailSignInForm = await superValidate(request, zod(signInFormSchema)); if (!emailSignInForm.valid) return fail(StatusCodes.BAD_REQUEST, { emailSignInForm }); - const { error } = await locals.api.iam.login.verify.$post({ json: emailSignInForm.data }).then(locals.parseApiResponse) + const { error } = await locals.api.auth.login.verify.$post({ json: emailSignInForm.data }).then(locals.parseApiResponse) if (error) return setError(emailSignInForm, 'token', error); redirect(301, '/'); } From 76d494711779d0b2204f52e87dec4b008385a12d Mon Sep 17 00:00:00 2001 From: Mariusz Wachowski Date: Thu, 17 Oct 2024 22:55:24 +0200 Subject: [PATCH 2/3] fixed getAuthedUserOrThrow type --- src/app.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.d.ts b/src/app.d.ts index 0b04506..28af1af 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -12,7 +12,7 @@ declare global { api: ApiClient['api']; parseApiResponse: typeof parseApiResponse; getAuthedUser: () => Promise | null>; - getAuthedUserOrThrow: (redirectTo: string) => Promise>; + getAuthedUserOrThrow: (redirectTo?: string) => Promise>; } // interface PageData {} From 91f36bcb9c2c87f402ad9703c4728e1ba6f4b79f Mon Sep 17 00:00:00 2001 From: Mariusz Wachowski Date: Thu, 17 Oct 2024 22:55:47 +0200 Subject: [PATCH 3/3] fixed verifyEmail action --- src/routes/(app)/settings/account/+page.server.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/routes/(app)/settings/account/+page.server.ts b/src/routes/(app)/settings/account/+page.server.ts index d91f350..7f48333 100644 --- a/src/routes/(app)/settings/account/+page.server.ts +++ b/src/routes/(app)/settings/account/+page.server.ts @@ -23,9 +23,8 @@ export const actions = { }, verifyEmail: async ({ request, locals }) => { const verifyEmailForm = await superValidate(request, zod(verifyEmailFormSchema)); - console.log(verifyEmailForm) if (!verifyEmailForm.valid) return fail(StatusCodes.BAD_REQUEST, { verifyEmailForm }) - const { error } = await locals.api.auth.email.verification.$post({ json: verifyEmailForm.data }).then(locals.parseApiResponse); + const { error } = await locals.api.auth.email.verify.$post({ json: verifyEmailForm.data }).then(locals.parseApiResponse); if (error) return setError(verifyEmailForm, 'token', error); return { verifyEmailForm } }