From 7c45a208660b49760c3c727a7cce0cb07800fdf6 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 11 Jun 2025 14:56:43 +0700 Subject: [PATCH] feat: add feature basic auth request OTP --- .../data/services/otp-verification.service.ts | 81 ++++++++++++++++--- .../infrastructure/guards/otp-auth-guard.ts | 80 ++++++++++++++++++ .../otp-verification-data.controller.ts | 21 +++-- .../otp-verification.module.ts | 14 +++- src/services/whatsapp/whatsapp.service.ts | 16 ++++ 5 files changed, 195 insertions(+), 17 deletions(-) create mode 100644 src/modules/configuration/otp-verification/infrastructure/guards/otp-auth-guard.ts diff --git a/src/modules/configuration/otp-verification/data/services/otp-verification.service.ts b/src/modules/configuration/otp-verification/data/services/otp-verification.service.ts index 148325e..e4d87ba 100644 --- a/src/modules/configuration/otp-verification/data/services/otp-verification.service.ts +++ b/src/modules/configuration/otp-verification/data/services/otp-verification.service.ts @@ -37,14 +37,69 @@ export class OtpVerificationService { return moment().valueOf(); // epoch millis verification time (now) } - async requestOTP(payload: OtpRequestEntity) { + private generateOTPMsgTemplate(payload) { + const { userRequest, newOtp } = payload; + const header = newOtp.action_type.split('_').join(' '); + const otpCode = newOtp?.otp_code; + const username = userRequest?.username; + const otpType = newOtp.action_type + .split('_') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(' '); + + return { + name: 'general_flow', + language: { code: 'id' }, + components: [ + { + type: 'header', + parameters: [ + { + type: 'text', + parameter_name: 'header', + text: header, + }, + ], + }, + { + type: 'body', + parameters: [ + { + type: 'text', + parameter_name: 'name', + text: username, + }, + { + type: 'text', + parameter_name: 'code', + text: otpCode, + }, + { + type: 'text', + parameter_name: 'type', + text: otpType, + }, + ], + }, + { + type: 'footer', + parameters: [ + { + type: 'text', + text: 'Kode berlaku selama 5 menit.', + }, + ], + }, + ], + }; + } + + async requestOTP(payload: OtpRequestEntity, req: any) { const otpService = new OtpService({ length: 4 }); const otpCode = otpService.generateSecureOTP(); const dateNow = this.generateTimestamp(); const expiredAt = this.generateOtpExpiration(); - - //TODO implementation from auth - const creator = { id: null, name: null }; + const userRequest = req?.user; const newOtp: OtpVerificationEntity = { otp_code: otpCode, @@ -56,13 +111,13 @@ export class OtpVerificationService { is_replaced: false, expired_at: expiredAt, - creator_id: creator.id, - creator_name: creator.name, + creator_id: userRequest?.id, + creator_name: userRequest?.name, created_at: dateNow, verified_at: null, - editor_id: creator.id, - editor_name: creator.name, + editor_id: userRequest?.id, + editor_name: userRequest?.name, updated_at: dateNow, }; @@ -95,9 +150,9 @@ export class OtpVerificationService { const notificationService = new WhatsappService(); verifiers.map((v) => { - notificationService.sendOtpNotification({ + notificationService.sendTemplateMessage({ phone: v.phone_number, - code: otpCode, + templateMsg: this.generateOTPMsgTemplate({ userRequest, newOtp }), }); }); @@ -108,7 +163,8 @@ export class OtpVerificationService { }; } - async verifyOTP(payload: OtpVerifyEntity) { + async verifyOTP(payload: OtpVerifyEntity, req: any) { + const userRequest = req?.user; const { otp_code, action_type, target_id, reference, source } = payload; const dateNow = this.generateTimestamp(); @@ -154,6 +210,9 @@ export class OtpVerificationService { otp.is_used = true; otp.verified_at = dateNow; + otp.editor_id = userRequest?.id; + otp.editor_name = userRequest?.name; + otp.updated_at = dateNow; // update otp to database await this.otpVerificationRepo.save(otp); diff --git a/src/modules/configuration/otp-verification/infrastructure/guards/otp-auth-guard.ts b/src/modules/configuration/otp-verification/infrastructure/guards/otp-auth-guard.ts new file mode 100644 index 0000000..2312e74 --- /dev/null +++ b/src/modules/configuration/otp-verification/infrastructure/guards/otp-auth-guard.ts @@ -0,0 +1,80 @@ +// auth/otp-auth.guard.ts +import { + CanActivate, + ExecutionContext, + Injectable, + UnprocessableEntityException, +} from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { validatePassword } from 'src/core/helpers/password/bcrypt.helpers'; +import { + CONNECTION_NAME, + STATUS, +} from 'src/core/strings/constants/base.constants'; +import { UserRole } from 'src/modules/user-related/user/constants'; +import { UserModel } from 'src/modules/user-related/user/data/models/user.model'; +import { DataSource, Not } from 'typeorm'; + +@Injectable() +export class OtpAuthGuard implements CanActivate { + constructor( + private readonly jwtService: JwtService, + + @InjectDataSource(CONNECTION_NAME.DEFAULT) + protected readonly dataSource: DataSource, + ) {} + + get userRepository() { + return this.dataSource.getRepository(UserModel); + } + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const jwtAuth = request.headers['authorization']; + const basicAuth = request.headers['basic_authorization']; + + // 1. Cek OTP Auth (basic_authorization header) + if (basicAuth) { + try { + const decoded = Buffer.from(basicAuth, 'base64').toString('ascii'); + const [username, password] = decoded.split('|'); + + const userLogin = await this.userRepository.findOne({ + where: { + username: username, + status: STATUS.ACTIVE, + role: Not(UserRole.QUEUE_ADMIN), + }, + }); + + const valid = await validatePassword(password, userLogin?.password); + + if (userLogin && valid) { + request.user = userLogin; + return true; + } else { + throw new UnprocessableEntityException('Invalid OTP credentials'); + } + } catch (err) { + throw new UnprocessableEntityException('Invalid OTP encoding'); + } + } + + // 2. Cek JWT (Authorization: Bearer ) + if (jwtAuth && jwtAuth.startsWith('Bearer ')) { + const token = jwtAuth.split(' ')[1]; + try { + const payload = await this.jwtService.verifyAsync(token); + request.user = payload; + return true; + } catch (err) { + throw new UnprocessableEntityException('Invalid JWT token'); + } + } + + throw new UnprocessableEntityException( + 'No valid authentication method found', + ); + } +} diff --git a/src/modules/configuration/otp-verification/infrastructure/otp-verification-data.controller.ts b/src/modules/configuration/otp-verification/infrastructure/otp-verification-data.controller.ts index 783f109..bd249ac 100644 --- a/src/modules/configuration/otp-verification/infrastructure/otp-verification-data.controller.ts +++ b/src/modules/configuration/otp-verification/infrastructure/otp-verification-data.controller.ts @@ -1,9 +1,18 @@ -import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { + Body, + Controller, + Get, + Param, + Post, + Req, + UseGuards, +} from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Public } from 'src/core/guards'; import { MODULE_NAME } from 'src/core/strings/constants/module.constants'; import { OtpVerificationService } from '../data/services/otp-verification.service'; import { OtpRequestDto, OtpVerifyDto } from './dto/otp-verification.dto'; +import { OtpAuthGuard } from './guards/otp-auth-guard'; //TODO implementation auth @ApiTags(`${MODULE_NAME.OTP_VERIFICATIONS.split('-').join(' ')} - data`) @@ -15,13 +24,15 @@ export class OtpVerificationController { ) {} @Post('request') - async request(@Body() body: OtpRequestDto) { - return await this.otpVerificationService.requestOTP(body); + @UseGuards(OtpAuthGuard) + async request(@Body() body: OtpRequestDto, @Req() req) { + return await this.otpVerificationService.requestOTP(body, req); } @Post('verify') - async verify(@Body() body: OtpVerifyDto) { - return await this.otpVerificationService.verifyOTP(body); + @UseGuards(OtpAuthGuard) + async verify(@Body() body: OtpVerifyDto, @Req() req) { + return await this.otpVerificationService.verifyOTP(body, req); } @Get(':ref_or_target_id') diff --git a/src/modules/configuration/otp-verification/otp-verification.module.ts b/src/modules/configuration/otp-verification/otp-verification.module.ts index 6e1f02d..1407b71 100644 --- a/src/modules/configuration/otp-verification/otp-verification.module.ts +++ b/src/modules/configuration/otp-verification/otp-verification.module.ts @@ -7,15 +7,27 @@ import { OtpVerificationModel } from './data/models/otp-verification.model'; import { OtpVerificationController } from './infrastructure/otp-verification-data.controller'; import { OtpVerificationService } from './data/services/otp-verification.service'; import { OtpVerifierModel } from './data/models/otp-verifier.model'; +import { OtpAuthGuard } from './infrastructure/guards/otp-auth-guard'; + +import { JwtModule } from '@nestjs/jwt'; +import { JWT_EXPIRED } from 'src/core/sessions/constants'; +import { JWT_SECRET } from 'src/core/sessions/constants'; + @Module({ imports: [ ConfigModule.forRoot(), + TypeOrmModule.forFeature( [OtpVerificationModel, OtpVerifierModel], CONNECTION_NAME.DEFAULT, ), + + JwtModule.register({ + secret: JWT_SECRET, + signOptions: { expiresIn: JWT_EXPIRED }, + }), ], controllers: [OtpVerificationController], - providers: [OtpVerificationService], + providers: [OtpAuthGuard, OtpVerificationService], }) export class OtpVerificationModule {} diff --git a/src/services/whatsapp/whatsapp.service.ts b/src/services/whatsapp/whatsapp.service.ts index cad1b36..fb07a2a 100644 --- a/src/services/whatsapp/whatsapp.service.ts +++ b/src/services/whatsapp/whatsapp.service.ts @@ -243,6 +243,22 @@ export class WhatsappService { } } + async sendTemplateMessage(data: { phone: string; templateMsg: any }) { + const payload = { + messaging_product: 'whatsapp', + to: data.phone, + type: 'template', + template: data?.templateMsg, + }; + + const response = await this.sendMessage(payload); + if (response) { + Logger.log( + `OTP notification for template ${data.templateMsg} sent to ${data.phone}`, + ); + } + } + async queueProcess(data: WhatsappQueue) { const queueUrl = `${WHATSAPP_BUSINESS_QUEUE_URL}?id=${data.id}`; const payload = { -- 2.40.1