fix: auth otp flow + remove generated
This commit is contained in:
@@ -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() { }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
4
src/mail/mail-job-names.ts
Normal file
4
src/mail/mail-job-names.ts
Normal 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
44
src/mail/mail.consumer.ts
Normal 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
|
||||
}
|
||||
}
|
||||
8
src/mail/mail.interface.ts
Normal file
8
src/mail/mail.interface.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface WelcomeEmailJob {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface RegisterOtpEmailJob {
|
||||
email: string;
|
||||
otp: number;
|
||||
}
|
||||
@@ -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],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user