Merge pull request 'feat: add feature basic auth request OTP' (#152) from feat/otp-cancel into development
Reviewed-on: #152pull/157/head 1.6.17-alpha.1
commit
ab9db39a5f
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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')
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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) {
|
||||
const queueUrl = `${WHATSAPP_BUSINESS_QUEUE_URL}?id=${data.id}`;
|
||||
const payload = {
|
||||
|
|
Loading…
Reference in New Issue