feat(nestjs): resend the auth confirmation message

This commit is contained in:
Ahmed Bouhuolia
2025-05-08 19:01:43 +02:00
parent f78d6efe27
commit 3c8b7c92fe
10 changed files with 149 additions and 20 deletions

View File

@@ -72,3 +72,7 @@ export interface IAuthSignUpVerifiedEventPayload {
verifyToken: string;
userId: number;
}
export interface ISignUpConfigmResendedEventPayload {
user: SystemUser;
}

View File

@@ -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,
],
})

View File

@@ -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<void>}
*/
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();
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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<void>}
*/
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);
}
}

View File

@@ -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<boolean>}
*/
async canActivate(context: ExecutionContext): Promise<boolean> {
const isIgnoredUserVerified = this.reflector.getAllAndOverride<boolean>(
IS_IGNORE_USER_VERIFIED,
[context.getHandler(), context.getClass()],
);
const isPublic = this.reflector.getAllAndOverride<boolean>(
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;
}
}

View File

@@ -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);
}