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)
|
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);
|
||||||
|
|
|
@ -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 { 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')
|
||||||
|
|
|
@ -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 {}
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
Loading…
Reference in New Issue