Merge pull request 'feat: add feature basic auth request OTP' (#152) from feat/otp-cancel into development

Reviewed-on: #152
pull/157/head 1.6.17-alpha.1
firmanr 2025-06-11 14:58:12 +07:00
commit ab9db39a5f
5 changed files with 195 additions and 17 deletions

View File

@ -37,14 +37,69 @@ export class OtpVerificationService {
return moment().valueOf(); // epoch millis verification time (now) 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 otpService = new OtpService({ length: 4 });
const otpCode = otpService.generateSecureOTP(); const otpCode = otpService.generateSecureOTP();
const dateNow = this.generateTimestamp(); const dateNow = this.generateTimestamp();
const expiredAt = this.generateOtpExpiration(); const expiredAt = this.generateOtpExpiration();
const userRequest = req?.user;
//TODO implementation from auth
const creator = { id: null, name: null };
const newOtp: OtpVerificationEntity = { const newOtp: OtpVerificationEntity = {
otp_code: otpCode, otp_code: otpCode,
@ -56,13 +111,13 @@ export class OtpVerificationService {
is_replaced: false, is_replaced: false,
expired_at: expiredAt, expired_at: expiredAt,
creator_id: creator.id, creator_id: userRequest?.id,
creator_name: creator.name, creator_name: userRequest?.name,
created_at: dateNow, created_at: dateNow,
verified_at: null, verified_at: null,
editor_id: creator.id, editor_id: userRequest?.id,
editor_name: creator.name, editor_name: userRequest?.name,
updated_at: dateNow, updated_at: dateNow,
}; };
@ -95,9 +150,9 @@ export class OtpVerificationService {
const notificationService = new WhatsappService(); const notificationService = new WhatsappService();
verifiers.map((v) => { verifiers.map((v) => {
notificationService.sendOtpNotification({ notificationService.sendTemplateMessage({
phone: v.phone_number, 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 { otp_code, action_type, target_id, reference, source } = payload;
const dateNow = this.generateTimestamp(); const dateNow = this.generateTimestamp();
@ -154,6 +210,9 @@ export class OtpVerificationService {
otp.is_used = true; otp.is_used = true;
otp.verified_at = dateNow; otp.verified_at = dateNow;
otp.editor_id = userRequest?.id;
otp.editor_name = userRequest?.name;
otp.updated_at = dateNow;
// update otp to database // update otp to database
await this.otpVerificationRepo.save(otp); await this.otpVerificationRepo.save(otp);

View File

@ -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<boolean> {
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 <token>)
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',
);
}
}

View File

@ -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 { ApiTags } from '@nestjs/swagger';
import { Public } from 'src/core/guards'; import { Public } from 'src/core/guards';
import { MODULE_NAME } from 'src/core/strings/constants/module.constants'; import { MODULE_NAME } from 'src/core/strings/constants/module.constants';
import { OtpVerificationService } from '../data/services/otp-verification.service'; import { OtpVerificationService } from '../data/services/otp-verification.service';
import { OtpRequestDto, OtpVerifyDto } from './dto/otp-verification.dto'; import { OtpRequestDto, OtpVerifyDto } from './dto/otp-verification.dto';
import { OtpAuthGuard } from './guards/otp-auth-guard';
//TODO implementation auth //TODO implementation auth
@ApiTags(`${MODULE_NAME.OTP_VERIFICATIONS.split('-').join(' ')} - data`) @ApiTags(`${MODULE_NAME.OTP_VERIFICATIONS.split('-').join(' ')} - data`)
@ -15,13 +24,15 @@ export class OtpVerificationController {
) {} ) {}
@Post('request') @Post('request')
async request(@Body() body: OtpRequestDto) { @UseGuards(OtpAuthGuard)
return await this.otpVerificationService.requestOTP(body); async request(@Body() body: OtpRequestDto, @Req() req) {
return await this.otpVerificationService.requestOTP(body, req);
} }
@Post('verify') @Post('verify')
async verify(@Body() body: OtpVerifyDto) { @UseGuards(OtpAuthGuard)
return await this.otpVerificationService.verifyOTP(body); async verify(@Body() body: OtpVerifyDto, @Req() req) {
return await this.otpVerificationService.verifyOTP(body, req);
} }
@Get(':ref_or_target_id') @Get(':ref_or_target_id')

View File

@ -7,15 +7,27 @@ import { OtpVerificationModel } from './data/models/otp-verification.model';
import { OtpVerificationController } from './infrastructure/otp-verification-data.controller'; import { OtpVerificationController } from './infrastructure/otp-verification-data.controller';
import { OtpVerificationService } from './data/services/otp-verification.service'; import { OtpVerificationService } from './data/services/otp-verification.service';
import { OtpVerifierModel } from './data/models/otp-verifier.model'; 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({ @Module({
imports: [ imports: [
ConfigModule.forRoot(), ConfigModule.forRoot(),
TypeOrmModule.forFeature( TypeOrmModule.forFeature(
[OtpVerificationModel, OtpVerifierModel], [OtpVerificationModel, OtpVerifierModel],
CONNECTION_NAME.DEFAULT, CONNECTION_NAME.DEFAULT,
), ),
JwtModule.register({
secret: JWT_SECRET,
signOptions: { expiresIn: JWT_EXPIRED },
}),
], ],
controllers: [OtpVerificationController], controllers: [OtpVerificationController],
providers: [OtpVerificationService], providers: [OtpAuthGuard, OtpVerificationService],
}) })
export class OtpVerificationModule {} export class OtpVerificationModule {}

View File

@ -429,6 +429,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) { async queueProcess(data: WhatsappQueue) {
const queueUrl = `${WHATSAPP_BUSINESS_QUEUE_URL}?id=${data.id}`; const queueUrl = `${WHATSAPP_BUSINESS_QUEUE_URL}?id=${data.id}`;
const payload = { const payload = {