fix: auth otp flow + remove generated

This commit is contained in:
SauravDhakal
2026-04-05 16:19:19 +05:45
parent 4905c6f1d1
commit ab8b2ef353
27 changed files with 340 additions and 8425 deletions

View File

@@ -20,7 +20,7 @@ import { Public } from './decorators';
@Controller('auth')
@Public()
export class AuthController {
constructor(private readonly authService: AuthService) {}
constructor(private readonly authService: AuthService) { }
@ApiOperation({ summary: 'User login' })
@HttpCode(HttpStatus.OK)
@@ -46,12 +46,12 @@ export class AuthController {
async register(@Body() body: RegisterUserRequestDTO): Promise<string> {
await this.authService.register(body);
return 'Registered successfully. Login to continue.';
return 'Check your email for OTP';
}
logout() {}
logout() { }
forgotPassword() {}
forgotPassword() { }
regenTokens() {}
regenTokens() { }
}

View File

@@ -1,4 +1,4 @@
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { ConflictException, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { Public } from './decorators';
import { LoginUserRequestDTO, RegisterUserRequestDTO } from './dto';
import * as bcrypt from 'bcrypt';
@@ -17,26 +17,75 @@ export class AuthService {
@InjectQueue('mail') private readonly mailQueue: Queue
) { }
// Generate OTP
async register(dto: RegisterUserRequestDTO) {
const hashedPassword = await bcrypt.hash(dto.password, 10);
await this.userService.createUserWithPassword({
...dto,
password: hashedPassword,
});
const [userExists, otpExists] = await Promise.all([
this.userService.findByEmail(dto.email),
this.userService.findByEmailInOTP(dto.email),
])
this.mailQueue.add('send-welcome-email', {
email: dto.email
if (userExists)
throw new ConflictException("User with this email already exists");
else if (otpExists) {
/* *
* If OTP was last generated more than 2 minutes ago, regen.
* Else, do nothing
* */
const now = Number(new Date()) / 1000;
const generatedOn = Number(otpExists.generatedOn) / 1000;
if (generatedOn + (60 * 2) > now) {
return;
}
}
const otp = this.genOtp()
await this.userService.updateOTPByEmail(dto.email, otp);
this.mailQueue.add('send-register-otp-email', {
email: dto.email,
otp: otp
}, {
attempts: 3,
backoff: {
type: "exponential",
delay: 3000,
delay: 3000
},
removeOnComplete: true, // clean up Redis after success
removeOnFail: false,
})
return true;
// const hashedPassword = await bcrypt.hash(dto.password, 10);
// await this.userService.createUserWithPassword({
// ...dto,
// password: hashedPassword,
// });
//
// this.mailQueue.add('send-welcome-email', {
// email: dto.email
// }, {
// attempts: 3,
// backoff: {
// type: "exponential",
// delay: 3000,
// },
// removeOnComplete: true, // clean up Redis after success
// removeOnFail: false,
// })
//
// return true;
}
// Validate OTP
async validateOtp() {
}
// Complete rest of singup process
async completeSignup() {
}
async login(dto: LoginUserRequestDTO) {
@@ -86,4 +135,9 @@ export class AuthService {
return { accessToken, refreshToken };
}
private genOtp() {
return 123456;
}
}

View File

@@ -0,0 +1,4 @@
export const MAIL_JOBS_NAME = {
WELCOME: 'send-welcome-email',
REGISTER_OTP: 'send-register-otp-email'
}

44
src/mail/mail.consumer.ts Normal file
View File

@@ -0,0 +1,44 @@
import { Processor, WorkerHost } from "@nestjs/bullmq";
import { Job } from "bullmq";
import { MailService } from "./mail.service";
import { MAIL_JOBS_NAME } from "./mail-job-names";
import { RegisterOtpEmailJob, WelcomeEmailJob } from "./mail.interface";
@Processor('mail')
export class MailConsumer extends WorkerHost {
constructor(private readonly mailService: MailService) {
super()
}
// This runs, so we define handlers here
async process(job: Job) {
const handlers: Record<string, (job: Job) => Promise<void>> = {
[MAIL_JOBS_NAME.REGISTER_OTP]: (j: Job<RegisterOtpEmailJob>) =>
this.handleSendOTPMail(j),
[MAIL_JOBS_NAME.WELCOME]: (j: Job<WelcomeEmailJob>) =>
this.handleSendWelcomeMail(j),
}
const handler = handlers[job.name];
if (!handler) throw new Error(`No handler for job: ${job.name}`);
await handler(job);
}
/*
* These are seperated. Using switch-case is not scalable, couldn't define types
* when there were multiple types of emails to be sent
* */
async handleSendOTPMail(job: Job<RegisterOtpEmailJob>) {
await this.mailService.sendOTPMail({
to: job.data.email,
otp: job.data.otp
})
return
}
async handleSendWelcomeMail(job: Job<WelcomeEmailJob>) {
await this.mailService.sendWelcomeMail({ to: job.data.email })
return
}
}

View File

@@ -0,0 +1,8 @@
export interface WelcomeEmailJob {
email: string;
}
export interface RegisterOtpEmailJob {
email: string;
otp: number;
}

View File

@@ -1,12 +1,12 @@
import { Module } from '@nestjs/common';
import { MailService } from './mail.service';
import { BullModule } from '@nestjs/bullmq';
import { MailConsumer } from './mail.processor';
import { MailConsumer } from './mail.consumer';
@Module({
imports: [
BullModule.registerQueue({
name: "welcome_mail"
name: "mail"
}),
],
providers: [MailService, MailConsumer],

View File

@@ -1,18 +0,0 @@
import { Processor, WorkerHost } from "@nestjs/bullmq";
import { Job } from "bullmq";
import { MailService } from "./mail.service";
@Processor('mail')
export class MailConsumer extends WorkerHost {
constructor(private readonly mailService: MailService) {
super()
}
async process(job: Job<{ email: string }>) {
switch (job.name) {
case 'send-welcome-email':
await this.mailService.sendWelcomeMail({ to: job.data.email })
break;
}
}
}

View File

@@ -49,6 +49,21 @@ export class MailService {
)
}
async sendOTPMail({ to, otp }: { to: string, otp: number }) {
if (!this.mailServiceAvailable)
throw new Error("Mail service not available")
const email = EmailTemplates.signup_otp(otp);
await this.transporter.sendMail(
{
to,
subject: email.subject,
html: email.body
}
)
}
sendMail({ to, subject, body }: { to: string, subject: string, body: string }) {
if (!this.mailServiceAvailable)
throw new Error("Mail service not available")

View File

@@ -14,8 +14,7 @@ import { ConfigService } from '@nestjs/config';
@Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleDestroy, OnModuleInit
{
implements OnModuleDestroy, OnModuleInit {
constructor(
private readonly ctx: RequestContextService,
private readonly configService: ConfigService,

View File

@@ -50,4 +50,40 @@ export class UserService {
data: { refreshToken },
});
}
/*
* USER OTP SERVICES
* */
async findByEmailInOTP(email: string) {
return await this.prisma.userOTP.findUnique({
where: {
email,
},
});
}
async removeByEmailInOTP(email: string) {
return await this.prisma.userOTP.delete({
where: {
email
}
})
}
async updateOTPByEmail(email: string, otp: number) {
return await this.prisma.userOTP.upsert({
where: {
email
},
create: {
email,
otp,
generatedOn: new Date()
},
update: {
otp,
generatedOn: new Date()
}
})
}
}