From 3c8b7c92fe11e08d7665529d90bd3479c2b70101 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 8 May 2025 19:01:43 +0200 Subject: [PATCH] feat(nestjs): resend the auth confirmation message --- packages/server/src/common/events/events.ts | 4 +- packages/server/src/modules/App/App.module.ts | 2 +- .../src/modules/Auth/Auth.interfaces.ts | 4 ++ .../server/src/modules/Auth/Auth.module.ts | 5 ++ .../modules/Auth/AuthApplication.sevice.ts | 13 ++++- .../src/modules/Auth/Authed.controller.ts | 34 +++++++++++-- .../Auth/commands/AuthSignin.service.ts | 5 -- .../AuthSignupConfirmResend.service.ts | 41 ++++++++++++++- .../Auth/guards/EnsureUserVerified.guard.ts | 50 +++++++++++++++++++ .../Auth/subscribers/AuthMail.subscriber.ts | 11 ++-- 10 files changed, 149 insertions(+), 20 deletions(-) create mode 100644 packages/server/src/modules/Auth/guards/EnsureUserVerified.guard.ts diff --git a/packages/server/src/common/events/events.ts b/packages/server/src/common/events/events.ts index c7db1e744..22c770b71 100644 --- a/packages/server/src/common/events/events.ts +++ b/packages/server/src/common/events/events.ts @@ -12,6 +12,9 @@ export const events = { signUpConfirming: 'signUpConfirming', signUpConfirmed: 'signUpConfirmed', + signUpConfirmResending: 'signUpConfirmResending', + signUpConfirmResended: 'signUpConfirmResended', + sendingResetPassword: 'onSendingResetPassword', sendResetPassword: 'onSendResetPassword', @@ -771,5 +774,4 @@ export const events = { onSalesByItemViewed: 'onSalesByItemViewed', onPurchasesByItemViewed: 'onPurchasesByItemViewed', }, - }; diff --git a/packages/server/src/modules/App/App.module.ts b/packages/server/src/modules/App/App.module.ts index c84084e81..e5170c207 100644 --- a/packages/server/src/modules/App/App.module.ts +++ b/packages/server/src/modules/App/App.module.ts @@ -144,11 +144,11 @@ import { CreditNotesApplyInvoiceModule } from '../CreditNotesApplyInvoice/Credit ScheduleModule.forRoot(), TenancyDatabaseModule, TenancyModelsModule, + AuthModule, TenancyModule, ChromiumlyTenancyModule, TransformerModule, MailModule, - AuthModule, ItemsModule, ItemCategoryModule, AccountsModule, diff --git a/packages/server/src/modules/Auth/Auth.interfaces.ts b/packages/server/src/modules/Auth/Auth.interfaces.ts index fcce0d264..6c720d48b 100644 --- a/packages/server/src/modules/Auth/Auth.interfaces.ts +++ b/packages/server/src/modules/Auth/Auth.interfaces.ts @@ -72,3 +72,7 @@ export interface IAuthSignUpVerifiedEventPayload { verifyToken: string; userId: number; } + +export interface ISignUpConfigmResendedEventPayload { + user: SystemUser; +} diff --git a/packages/server/src/modules/Auth/Auth.module.ts b/packages/server/src/modules/Auth/Auth.module.ts index b220ced4c..644446ec4 100644 --- a/packages/server/src/modules/Auth/Auth.module.ts +++ b/packages/server/src/modules/Auth/Auth.module.ts @@ -31,6 +31,7 @@ import { GetAuthMetaService } from './queries/GetAuthMeta.service'; import { AuthedController } from './Authed.controller'; import { GetAuthenticatedAccount } from './queries/GetAuthedAccount.service'; import { TenancyModule } from '../Tenancy/Tenancy.module'; +import { EnsureUserVerifiedGuard } from './guards/EnsureUserVerified.guard'; const models = [InjectSystemModel(PasswordReset)]; @@ -73,6 +74,10 @@ const models = [InjectSystemModel(PasswordReset)]; provide: APP_GUARD, useClass: JwtAuthGuard, }, + { + provide: APP_GUARD, + useClass: EnsureUserVerifiedGuard, + }, AuthMailSubscriber, ], }) diff --git a/packages/server/src/modules/Auth/AuthApplication.sevice.ts b/packages/server/src/modules/Auth/AuthApplication.sevice.ts index 6b14892c9..e4e16d4e2 100644 --- a/packages/server/src/modules/Auth/AuthApplication.sevice.ts +++ b/packages/server/src/modules/Auth/AuthApplication.sevice.ts @@ -8,6 +8,7 @@ import { AuthSignupDto } from './dtos/AuthSignup.dto'; import { AuthSendResetPasswordService } from './commands/AuthSendResetPassword.service'; import { AuthResetPasswordService } from './commands/AuthResetPassword.service'; import { GetAuthMetaService } from './queries/GetAuthMeta.service'; +import { GetAuthenticatedAccount } from './queries/GetAuthedAccount.service'; @Injectable() export class AuthenticationApplication { @@ -19,6 +20,7 @@ export class AuthenticationApplication { private readonly authResetPasswordService: AuthResetPasswordService, private readonly authSendResetPasswordService: AuthSendResetPasswordService, private readonly authGetMeta: GetAuthMetaService, + private readonly getAuthedAccountService: GetAuthenticatedAccount, ) {} /** @@ -53,8 +55,8 @@ export class AuthenticationApplication { * @param {number} userId - System user id. * @returns {Promise} */ - public async signUpConfirmResend(userId: number) { - return this.authSignUpConfirmResendService.signUpConfirmResend(userId); + public async signUpConfirmResend() { + return this.authSignUpConfirmResendService.signUpConfirmResend(); } /** @@ -83,4 +85,11 @@ export class AuthenticationApplication { public async getAuthMeta() { return this.authGetMeta.getAuthMeta(); } + + /** + * Retrieves the authenticated account meta. + */ + public getAuthedAccount() { + return this.getAuthedAccountService.getAccount(); + } } diff --git a/packages/server/src/modules/Auth/Authed.controller.ts b/packages/server/src/modules/Auth/Authed.controller.ts index 846ab9dab..07cb48e5e 100644 --- a/packages/server/src/modules/Auth/Authed.controller.ts +++ b/packages/server/src/modules/Auth/Authed.controller.ts @@ -1,23 +1,47 @@ -import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiBody, ApiOperation, ApiTags } from '@nestjs/swagger'; import { GetAuthenticatedAccount } from './queries/GetAuthedAccount.service'; -import { Controller, Get } from '@nestjs/common'; +import { Controller, Get, Post } from '@nestjs/common'; import { IgnoreTenantSeededRoute } from '../Tenancy/EnsureTenantIsSeeded.guards'; import { IgnoreTenantInitializedRoute } from '../Tenancy/EnsureTenantIsInitialized.guard'; +import { AuthenticationApplication } from './AuthApplication.sevice'; +import { TenancyContext } from '../Tenancy/TenancyContext.service'; +import { IgnoreUserVerifiedRoute } from './guards/EnsureUserVerified.guard'; @Controller('/auth') @ApiTags('Auth') @IgnoreTenantSeededRoute() @IgnoreTenantInitializedRoute() +@IgnoreUserVerifiedRoute() export class AuthedController { constructor( private readonly getAuthedAccountService: GetAuthenticatedAccount, + private readonly authApp: AuthenticationApplication, + private readonly tenancyContext: TenancyContext, ) {} + @Post('/signup/confirm/resend') + @ApiOperation({ summary: 'Resend the signup confirmation message' }) + @ApiBody({ + schema: { + type: 'object', + properties: { + code: { type: 'number', example: 200 }, + message: { type: 'string', example: 'resent successfully.' }, + }, + }, + }) + async resendSignupConfirm() { + await this.authApp.signUpConfirmResend(); + + return { + code: 200, + message: 'The signup confirmation message has been resent successfully.', + }; + } + @Get('/account') @ApiOperation({ summary: 'Retrieve the authenticated account' }) async getAuthedAcccount() { - const data = await this.getAuthedAccountService.getAccount(); - - return { data }; + return this.getAuthedAccountService.getAccount(); } } diff --git a/packages/server/src/modules/Auth/commands/AuthSignin.service.ts b/packages/server/src/modules/Auth/commands/AuthSignin.service.ts index 836f1b507..0ca89fe6f 100644 --- a/packages/server/src/modules/Auth/commands/AuthSignin.service.ts +++ b/packages/server/src/modules/Auth/commands/AuthSignin.service.ts @@ -41,11 +41,6 @@ export class AuthSigninService { `Wrong password for user with email: ${email}`, ); } - if (!user.verified) { - throw new UnauthorizedException( - `The user is not verified yet, check out your mail inbox.`, - ); - } return user; } diff --git a/packages/server/src/modules/Auth/commands/AuthSignupConfirmResend.service.ts b/packages/server/src/modules/Auth/commands/AuthSignupConfirmResend.service.ts index 40444be5c..3f923a032 100644 --- a/packages/server/src/modules/Auth/commands/AuthSignupConfirmResend.service.ts +++ b/packages/server/src/modules/Auth/commands/AuthSignupConfirmResend.service.ts @@ -1,5 +1,42 @@ +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { Inject, Injectable } from '@nestjs/common'; +import { SystemUser } from '@/modules/System/models/SystemUser'; +import { ServiceError } from '@/modules/Items/ServiceError'; +import { ERRORS } from '../Auth.constants'; +import { events } from '@/common/events/events'; +import { ModelObject } from 'objection'; +import { ISignUpConfigmResendedEventPayload } from '../Auth.interfaces'; +import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; + +@Injectable() export class AuthSignupConfirmResendService { - signUpConfirmResend(userId: number) { - return; + constructor( + private readonly eventPublisher: EventEmitter2, + private readonly tenancyContext: TenancyContext, + + @Inject(SystemUser.name) + private readonly systemUserModel: typeof SystemUser, + ) {} + + /** + * Resends the email confirmation of the given user. + * @param {number} userId - System User ID. + * @returns {Promise} + */ + public async signUpConfirmResend() { + const user = await this.tenancyContext.getSystemUser(); + + // Throw error if the user is already verified. + if (user.verified) { + throw new ServiceError(ERRORS.USER_ALREADY_VERIFIED); + } + // Throw error if the verification token is not exist. + if (!user.verifyToken) { + throw new ServiceError(ERRORS.USER_ALREADY_VERIFIED); + } + // Triggers `signUpConfirmResended` event. + await this.eventPublisher.emitAsync(events.auth.signUpConfirmResended, { + user, + } as ISignUpConfigmResendedEventPayload); } } diff --git a/packages/server/src/modules/Auth/guards/EnsureUserVerified.guard.ts b/packages/server/src/modules/Auth/guards/EnsureUserVerified.guard.ts new file mode 100644 index 000000000..12b026a7f --- /dev/null +++ b/packages/server/src/modules/Auth/guards/EnsureUserVerified.guard.ts @@ -0,0 +1,50 @@ +import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; +import { + CanActivate, + ExecutionContext, + Injectable, + SetMetadata, + UnauthorizedException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { IS_PUBLIC_ROUTE } from '../Auth.constants'; + +export const IS_IGNORE_USER_VERIFIED = 'IS_IGNORE_USER_VERIFIED'; +export const IgnoreUserVerifiedRoute = () => + SetMetadata(IS_IGNORE_USER_VERIFIED, true); + +@Injectable() +export class EnsureUserVerifiedGuard implements CanActivate { + constructor( + private readonly tenancyContext: TenancyContext, + private readonly reflector: Reflector, + ) {} + + /** + * Validate the authenticated user if verified and throws exception if not. + * @param {ExecutionContext} context + * @returns {Promise} + */ + async canActivate(context: ExecutionContext): Promise { + const isIgnoredUserVerified = this.reflector.getAllAndOverride( + IS_IGNORE_USER_VERIFIED, + [context.getHandler(), context.getClass()], + ); + const isPublic = this.reflector.getAllAndOverride( + IS_PUBLIC_ROUTE, + [context.getHandler(), context.getClass()], + ); + // Skip the guard early if the route marked as public or ignored. + if (isPublic || isIgnoredUserVerified) { + return true; + } + const systemUser = await this.tenancyContext.getSystemUser(); + + if (!systemUser.verified) { + throw new UnauthorizedException( + `The user is not verified yet, check out your mail inbox.`, + ); + } + return true; + } +} diff --git a/packages/server/src/modules/Auth/subscribers/AuthMail.subscriber.ts b/packages/server/src/modules/Auth/subscribers/AuthMail.subscriber.ts index 8af9cbc8f..8878e5280 100644 --- a/packages/server/src/modules/Auth/subscribers/AuthMail.subscriber.ts +++ b/packages/server/src/modules/Auth/subscribers/AuthMail.subscriber.ts @@ -4,6 +4,7 @@ import { OnEvent } from '@nestjs/event-emitter'; import { IAuthSendedResetPassword, IAuthSignedUpEventPayload, + ISignUpConfigmResendedEventPayload, } from '../Auth.interfaces'; import { Queue } from 'bullmq'; import { InjectQueue } from '@nestjs/bullmq'; @@ -27,12 +28,15 @@ export class AuthMailSubscriber { ) {} /** - * @param {IAuthSignedUpEventPayload} payload + * @param {IAuthSignedUpEventPayload | ISignUpConfigmResendedEventPayload} payload */ @OnEvent(events.auth.signUp) - async handleSignupSendVerificationMail(payload: IAuthSignedUpEventPayload) { + @OnEvent(events.auth.signUpConfirmResended) + async handleSignupSendVerificationMail( + payload: IAuthSignedUpEventPayload | ISignUpConfigmResendedEventPayload, + ) { try { - const job = await this.sendSignupVerificationMailQueue.add( + await this.sendSignupVerificationMailQueue.add( SendSignupVerificationMailJob, { email: payload.user.email, @@ -43,7 +47,6 @@ export class AuthMailSubscriber { delay: 0, }, ); - console.log(job); } catch (error) { console.log(error); }