diff --git a/src/constants/kafkajs-consumer-options.ts b/src/constants/kafkajs-consumer-options.ts deleted file mode 100644 index eaef49c..0000000 --- a/src/constants/kafkajs-consumer-options.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ConsumerConfig, ConsumerSubscribeTopics, KafkaMessage } from 'kafkajs'; - -export interface kafkaConsumerOptions { - topic: ConsumerSubscribeTopics; - config: ConsumerConfig; - onMessage: (message: KafkaMessage) => Promise; -} diff --git a/src/constants/sports-enum.ts b/src/constants/sports-enum.ts index 650b89d..2dce22d 100644 --- a/src/constants/sports-enum.ts +++ b/src/constants/sports-enum.ts @@ -5,4 +5,14 @@ enum SportsEnum { Basketball = 'Basketball', Volleyball = 'Volleyball', } + +export const SportsEnumList = [ + SportsEnum.Football, + SportsEnum.Badminton, + SportsEnum.Basketball, + SportsEnum.Cricket, + SportsEnum.Football, + SportsEnum.Volleyball, +]; + export default SportsEnum; diff --git a/src/services/apis/otp/dto/generateOtp.dto.ts b/src/services/apis/otp/dto/generateOtp.dto.ts new file mode 100644 index 0000000..d6e7625 --- /dev/null +++ b/src/services/apis/otp/dto/generateOtp.dto.ts @@ -0,0 +1,23 @@ +import { date, z } from 'zod'; + +// DTO for incoming OTP payload. This ensures the payload structure and validates fields. +/** + * The email of the user to send OTP. + * @example "test@gmail.com" + * */ + +export const OtpValidation = z.object({ + email: z.string().email(), +}); + +/** + * The email of the user to send OTP. + * @example " + * */ +export const VerifyOtpValidation = z.object({ + email: z.string().email(), + otp: z.string().trim(), +}); + +export type OtpDto = z.infer; +export type VerifyOtpDto = z.infer; diff --git a/src/services/apis/otp/dto/genrateOtp.dto.ts b/src/services/apis/otp/dto/genrateOtp.dto.ts deleted file mode 100644 index 013ccfe..0000000 --- a/src/services/apis/otp/dto/genrateOtp.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -// DTO for incoming OTP payload. This ensures the payload structure and validates fields. -export class OtpDto { - /** - * The email of the user to send OTP. - * @example "test@gmail.com" - * */ - - email: string; -} - -export class VerifyOtpDto { - /** - * The email of the user to send OTP. - * @example " - * */ - - email: string; - otp: string; -} diff --git a/src/services/apis/otp/generateOtp.controller.ts b/src/services/apis/otp/generateOtp.controller.ts index e0c879c..54b86cd 100644 --- a/src/services/apis/otp/generateOtp.controller.ts +++ b/src/services/apis/otp/generateOtp.controller.ts @@ -1,22 +1,29 @@ import { Body, Controller, Post } from '@nestjs/common'; -import { GenrateOtpService } from './genrateOtp.service'; -import { OtpDto, VerifyOtpDto } from './dto/genrateOtp.dto'; +import { GenerateOtpService } from './generateOtp.service'; +import { + OtpDto, + OtpValidation, + VerifyOtpDto, + VerifyOtpValidation, +} from './dto/generateOtp.dto'; import { Public } from '../auth/decorators/public.decorator'; @Controller('otp') export class GenerateOtpController { - constructor(private readonly genrateOtpService: GenrateOtpService) {} + constructor(private readonly generateOtpService: GenerateOtpService) {} @Public() @Post('generate') - async create(@Body() genrateOtpDto: OtpDto) { - return await this.genrateOtpService.enqueueOtpJob(genrateOtpDto); + async create(@Body() generateOtpDto: OtpDto) { + generateOtpDto = OtpValidation.parse(generateOtpDto); + return await this.generateOtpService.enqueueOtpJob(generateOtpDto); } @Public() @Post('verify') async verify(@Body() verifyOtpDto: VerifyOtpDto) { - return (await this.genrateOtpService.compareOtp(verifyOtpDto)) + verifyOtpDto = VerifyOtpValidation.parse(verifyOtpDto); + return (await this.generateOtpService.compareOtp(verifyOtpDto)) ? 'OTP is correct' : 'OTP is incorrect'; } diff --git a/src/services/apis/otp/generateOtp.module.ts b/src/services/apis/otp/generateOtp.module.ts index e35258b..6a94cb1 100644 --- a/src/services/apis/otp/generateOtp.module.ts +++ b/src/services/apis/otp/generateOtp.module.ts @@ -1,12 +1,11 @@ import { Module } from '@nestjs/common'; import { GenerateOtpController } from './generateOtp.controller'; -import { QueueModule } from 'src/services/bullmq/queue.module'; -import { GenrateOtpService } from './genrateOtp.service'; +import { GenerateOtpService } from './generateOtp.service'; @Module({ - imports: [QueueModule], + imports: [], controllers: [GenerateOtpController], - providers: [GenrateOtpService], - exports: [GenrateOtpService], // not exporting services as no need in testing + providers: [GenerateOtpService], + exports: [GenerateOtpService], // not exporting services as no need in testing (??) }) export class GenerateOtpModule {} diff --git a/src/services/apis/otp/generateOtp.service.ts b/src/services/apis/otp/generateOtp.service.ts new file mode 100644 index 0000000..e5f51ed --- /dev/null +++ b/src/services/apis/otp/generateOtp.service.ts @@ -0,0 +1,46 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { OtpProducer } from 'src/services/bullmq/producers/otp.producer'; +import { OtpDto, VerifyOtpDto } from './dto/generateOtp.dto'; +// import { processEmail } from './helpers/generateOtp.helper'; +import { OtpQueueProcessor } from 'src/services/bullmq/processors/otp.processor'; +import { compareHashedString } from 'src/common/hashing'; +import { RedisService } from 'src/services/redis/redis.service'; +import { generateKey } from 'src/services/bullmq/constants/generate-keys'; + +@Injectable() +export class GenerateOtpService { + constructor( + private readonly otpProducer: OtpProducer, + private readonly redisService: RedisService, + ) {} + + async enqueueOtpJob(createOtpDto: OtpDto) { + const email = createOtpDto['email'].trim(); + const resp = await this.otpProducer.pushForAsyncMailing( + `process-otp`, + { email }, + { + removeOnComplete: true, + }, + ); + return resp; + } + + async compareOtp( + verifyOtpDto: VerifyOtpDto, + removeEntryAfterCheck = false, + ): Promise { + const key = generateKey(verifyOtpDto.email); + const val = await this.redisService.get(key); + + if (!val) { + throw new BadRequestException( + 'The OTP has expired or no request for an OTP was found. Please try requesting a new OTP.', + ); + } + + const resp = await compareHashedString(verifyOtpDto.otp, val); + if (removeEntryAfterCheck) await this.redisService.del(key); + return resp; + } +} diff --git a/src/services/apis/otp/genrateOtp.helper.ts b/src/services/apis/otp/genrateOtp.helper.ts deleted file mode 100644 index 4418a6d..0000000 --- a/src/services/apis/otp/genrateOtp.helper.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { z } from 'zod'; - -const emailSchema = z.string().email(); - -export function processEmail(input: { email: string }): { email: string } { - const parsedEmail = emailSchema.safeParse(input.email); - - if (!parsedEmail.success) { - throw new Error('Invalid email: Must be a valid email format'); - } - - return { - email: input.email.trim(), - }; -} diff --git a/src/services/apis/otp/genrateOtp.service.ts b/src/services/apis/otp/genrateOtp.service.ts deleted file mode 100644 index 2f5f553..0000000 --- a/src/services/apis/otp/genrateOtp.service.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { OtpProducer } from 'src/services/bullmq/producers/otp.producer'; -import { OtpDto, VerifyOtpDto } from './dto/genrateOtp.dto'; -import { processEmail } from './genrateOtp.helper'; -import { OtpQueueProcessor } from 'src/services/bullmq/processors/otp.processor'; - -@Injectable() -export class GenrateOtpService { - constructor( - private readonly otpProducer: OtpProducer, - private readonly processor: OtpQueueProcessor, - ) {} - - async enqueueOtpJob(createOtpDto: OtpDto) { - const email = processEmail(createOtpDto); - await this.otpProducer.pushForAsyncStream(`process-otp`, email, { - removeOnComplete: true, - }); - } - - async compareOtp(verifyOtpDto: VerifyOtpDto): Promise { - return await this.processor.comapreOtp( - verifyOtpDto.email, - verifyOtpDto.otp, - ); - } -} diff --git a/src/services/apis/otp/helpers/generateOtp.helper.ts b/src/services/apis/otp/helpers/generateOtp.helper.ts new file mode 100644 index 0000000..942da3b --- /dev/null +++ b/src/services/apis/otp/helpers/generateOtp.helper.ts @@ -0,0 +1,15 @@ +// import { z } from 'zod'; + +// const emailSchema = z.string().email(); + +// export function processEmail(input: { email: string }): { email: string } { +// const parsedEmail = emailSchema.safeParse(input.email); + +// if (!parsedEmail.success) { +// throw new Error('Invalid email: Must be a valid email format'); +// } + +// return { +// email: input.email.trim(), +// }; +// } diff --git a/src/services/apis/profiles/schemas/profiles.schema.ts b/src/services/apis/profiles/schemas/profiles.schema.ts index a56353d..8cdc922 100644 --- a/src/services/apis/profiles/schemas/profiles.schema.ts +++ b/src/services/apis/profiles/schemas/profiles.schema.ts @@ -3,7 +3,7 @@ import { HydratedDocument, Types } from 'mongoose'; import { SoftDeleteSchema } from 'src/common/soft-delete-schema'; import EnsureObjectId from 'src/common/EnsureObjectId'; import { Users } from '../../users/schemas/users.schema'; -import SportsEnum from 'src/constants/sports-enum'; +import SportsEnum, { SportsEnumList } from 'src/constants/sports-enum'; export type ProfilesDocument = HydratedDocument; @@ -13,7 +13,7 @@ export type ProfilesDocument = HydratedDocument; export class Profiles extends SoftDeleteSchema { @Prop({ type: String, - enum: Object.values(SportsEnum), + enum: SportsEnumList, required: true, }) sport: SportsEnum; diff --git a/src/services/apis/users/users.controller.ts b/src/services/apis/users/users.controller.ts index 528c6a3..3b695aa 100644 --- a/src/services/apis/users/users.controller.ts +++ b/src/services/apis/users/users.controller.ts @@ -15,11 +15,14 @@ import { Users } from './schemas/users.schema'; import { Public } from '../auth/decorators/public.decorator'; import * as bcrypt from 'bcrypt'; import { JwtService } from '@nestjs/jwt'; +import { CreateUsersDTO } from './dto/users.dto'; +import { GenerateOtpService } from '../otp/generateOtp.service'; @Controller('users') export class UsersController { constructor( private readonly usersService: UsersService, - private jwtService: JwtService, + private readonly jwtService: JwtService, + private readonly generateOtpService: GenerateOtpService, ) {} @Get() @@ -35,7 +38,20 @@ export class UsersController { @Public() @Post() async create(@Body() createUsersDto: Users) { - await this.verifyOTP(createUsersDto); + /** + * @todo use zod for validations... + */ + if (!createUsersDto.email || !createUsersDto['otp']) { + throw new BadRequestException('Email or OTP not provided!'); + } + + await this.generateOtpService.compareOtp( + { + email: createUsersDto.email.trim(), + otp: String(createUsersDto['otp']).trim(), + }, + true, // removeEntryAfterCheck + ); const saltOrRounds = 10; const password = await bcrypt.hash(createUsersDto.password, saltOrRounds); @@ -45,7 +61,6 @@ export class UsersController { password, })) as Users; - // await this.removeOTP(createUsersDto.email); const sanitizedUser = this.usersService.sanitizeUser(user); const payload = { sub: { id: user._id }, user }; @@ -68,11 +83,4 @@ export class UsersController { async delete(@Param('id') id, @Query() query, @User() user) { return await this.usersService._remove(id, query, user); } - - async verifyOTP(createUsersDto: Users) { - if (!Object.keys(createUsersDto).includes('otp')) { - throw new BadRequestException('OTP not provided!'); - } - console.log({ createUsersDto }); - } } diff --git a/src/services/apis/users/users.module.ts b/src/services/apis/users/users.module.ts index c37364e..f2d8875 100644 --- a/src/services/apis/users/users.module.ts +++ b/src/services/apis/users/users.module.ts @@ -5,10 +5,12 @@ import { UsersService } from './users.service'; import { Users, UsersSchema } from './schemas/users.schema'; import { APP_GUARD } from '@nestjs/core'; import { RolesGuard } from './roles.guard'; +import { GenerateOtpModule } from '../otp/generateOtp.module'; @Module({ imports: [ MongooseModule.forFeature([{ name: Users.name, schema: UsersSchema }]), + GenerateOtpModule, ], controllers: [UsersController], providers: [ diff --git a/src/services/bullmq/constants/generate-keys.ts b/src/services/bullmq/constants/generate-keys.ts new file mode 100644 index 0000000..127d342 --- /dev/null +++ b/src/services/bullmq/constants/generate-keys.ts @@ -0,0 +1,3 @@ +export const generateKey = (email: string): string => + `verification::email:${email}`; +// more keys diff --git a/src/services/bullmq/processors/otp.processor.ts b/src/services/bullmq/processors/otp.processor.ts index 4ae9a67..ab43a91 100644 --- a/src/services/bullmq/processors/otp.processor.ts +++ b/src/services/bullmq/processors/otp.processor.ts @@ -3,7 +3,7 @@ import { OTP_QUEUE } from '../constants/queues'; import { RedisService } from 'src/services/redis/redis.service'; import { Job } from 'bullmq'; import { OTP_TTL } from 'src/constants/otp.constants'; -import { hashString } from 'src/common/hashing'; +import { compareHashedString, hashString } from 'src/common/hashing'; import { OtpJob } from '../jobs/otp.jobs'; import generateRandomNumber from 'src/common/generate-random-number'; import { Injectable } from '@nestjs/common'; @@ -19,7 +19,7 @@ export class OtpQueueProcessor extends WorkerHost { super(); } otp: string = generateRandomNumber(); - async process(job: Job): Promise { + async process(job: Job) { try { const { data: { email }, @@ -28,8 +28,15 @@ export class OtpQueueProcessor extends WorkerHost { console.log(`OTP for ${email} is ${this.otp} and stored in Redis`); await this.mailerService.sendMail(email, this.otp, 'partials/otp.hbs'); console.log(`OTP email sent to ${email}`); + return { + success: true, + }; } catch (error) { console.error('Error in processing OTP job', error); + return { + success: false, + message: error, + }; } } @@ -40,14 +47,14 @@ export class OtpQueueProcessor extends WorkerHost { await this.redisService.set(key, value, OTP_TTL); } - async comapreOtp(email: string, otp: string): Promise { - const key = await generateKey(email); + async compareOtp(email: string, otp: string): Promise { + const key = generateKey(email); const hashedOtp = await hashString(otp); const storedOtp = await this.redisService.get(key); return hashedOtp === storedOtp; } } -async function generateKey(email: string): Promise { +function generateKey(email: string): string { return `verification::email:${email}`; } diff --git a/src/services/bullmq/producers/otp.producer.ts b/src/services/bullmq/producers/otp.producer.ts index 1e58b81..d02bea6 100644 --- a/src/services/bullmq/producers/otp.producer.ts +++ b/src/services/bullmq/producers/otp.producer.ts @@ -6,12 +6,17 @@ import { InjectQueue } from '@nestjs/bullmq'; @Injectable() export class OtpProducer { constructor(@InjectQueue(OTP_QUEUE) private otpQueue: Queue) {} - async pushForAsyncStream( + async pushForAsyncMailing( jobName: string, data: Record, options?: JobsOptions, ) { await this.otpQueue.add(jobName, data, options); console.log(`Job "${jobName}" added to the queue successfully.`); + return { + success: true, + message: `Your OTP request is being processed. We'll send the OTP to your email shortly.`, + jobName, + }; } } diff --git a/yarn.lock b/yarn.lock index 1600bd5..3665012 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1281,6 +1281,11 @@ dependencies: "@types/node" "*" +"@types/bcryptjs@^2.4.6": + version "2.4.6" + resolved "https://registry.yarnpkg.com/@types/bcryptjs/-/bcryptjs-2.4.6.tgz#2b92e3c2121c66eba3901e64faf8bb922ec291fa" + integrity sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ== + "@types/body-parser@*": version "1.19.5" resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz" @@ -2092,6 +2097,11 @@ bcrypt@^5.1.1: "@mapbox/node-pre-gyp" "^1.0.11" node-addon-api "^5.0.0" +bcryptjs@^2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb" + integrity sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ== + binary-extensions@^2.0.0: version "2.3.0" resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz"