Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions src/api/withdrawal/withdrawal.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,14 +252,29 @@ export class WithdrawalService {
);
return { error: otpError };
} else {
const otpResponse = await this.otpService.verifyOtpCode(
otpCode,
userInfo,
reference_type.WITHDRAW_PAYMENT,
);
try {
const otpResponse = await this.otpService.verifyOtpCode(
otpCode,
userInfo,
reference_type.WITHDRAW_PAYMENT,
);

if (!otpResponse || otpResponse.code !== 'success') {
return { error: otpResponse };
if (!otpResponse || otpResponse.code !== 'success') {
return { error: otpResponse };
}
} catch (error) {
if (error.code === 'P2010' && error.meta?.code === '55P03') {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
Consider using a more specific error type or class for the thrown error instead of a generic Error. This can improve error handling and make it easier to distinguish different error types in the codebase.

this.logger.error(
'Payment request denied because payment row was locked previously!',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ security]
Logging the error object directly might expose sensitive information. Consider sanitizing the error details before logging to ensure no sensitive data is exposed.

error,
);

throw new Error(
'Some or all of the winnings you requested to process are either processing, on hold or already paid.',
);
} else {
throw error;
}
}
}

Expand Down
113 changes: 58 additions & 55 deletions src/shared/global/otp.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import crypto from 'crypto';
import { reference_type } from '@prisma/client';
import { otp, reference_type } from '@prisma/client';
import { ENV_CONFIG } from 'src/config';
import { TopcoderEmailService } from '../topcoder/tc-email.service';
import { BasicMemberInfo } from '../topcoder';
Expand Down Expand Up @@ -104,62 +104,65 @@ export class OtpService {
userInfo: BasicMemberInfo,
actionType: reference_type,
) {
const record = await this.prisma.otp.findFirst({
where: {
otp_hash: hashOtp(otpCode),
},
orderBy: {
expiration_time: 'desc',
},
});

if (!record) {
this.logger.warn(`No OTP record found for the provided code.`);
return { code: 'otp_invalid', message: `Invalid OTP code.` };
}

if (record.email !== userInfo.email) {
this.logger.warn(`Email mismatch for OTP verification.`);
return {
code: 'otp_email_mismatch',
message: `Email mismatch for OTP verification.`,
};
}

if (record.action_type !== actionType) {
this.logger.warn(`Action type mismatch for OTP verification.`);
return {
code: 'otp_action_type_mismatch',
message: `Action type mismatch for OTP verification.`,
};
}

if (record.expiration_time && record.expiration_time < new Date()) {
this.logger.warn(`OTP code has expired.`);
return { code: 'otp_expired', message: `OTP code has expired.` };
}

if (record.verified_at !== null) {
this.logger.warn(`OTP code has already been verified.`);
return {
code: 'otp_already_verified',
message: `OTP code has already been verified.`,
};
}
return await this.prisma.$transaction(async (tx) => {
const records = await tx.$queryRaw<otp>`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ security]
Using tx.$queryRaw with raw SQL queries can expose the application to SQL injection vulnerabilities if the input is not properly sanitized. Ensure that hashOtp(otpCode) is sanitized or consider using parameterized queries to prevent SQL injection.

SELECT id, email, otp_hash, expiration_time, action_type, created_at, updated_at, verified_at
FROM otp
WHERE otp_hash=${hashOtp(otpCode)}
ORDER BY expiration_time DESC
LIMIT 1
FOR UPDATE NOWAIT;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The use of FOR UPDATE NOWAIT can lead to exceptions if the row is locked by another transaction. Consider handling potential exceptions to ensure the application can gracefully handle such scenarios.

`;
const record = records[0];

if (!record) {
this.logger.warn(`No OTP record found for the provided code.`);
return { code: 'otp_invalid', message: `Invalid OTP code.` };
}

if (record.email !== userInfo.email) {
this.logger.warn(`Email mismatch for OTP verification.`);
return {
code: 'otp_email_mismatch',
message: `Email mismatch for OTP verification.`,
};
}

if (record.action_type !== actionType) {
this.logger.warn(`Action type mismatch for OTP verification.`);
return {
code: 'otp_action_type_mismatch',
message: `Action type mismatch for OTP verification.`,
};
}

if (record.expiration_time && record.expiration_time < new Date()) {
this.logger.warn(`OTP code has expired.`);
return { code: 'otp_expired', message: `OTP code has expired.` };
}

if (record.verified_at !== null) {
this.logger.warn(`OTP code has already been verified.`);
return {
code: 'otp_already_verified',
message: `OTP code has already been verified.`,
};
}

this.logger.log(
`OTP code ${otpCode} verified successfully for action ${actionType}`,
);

this.logger.log(
`OTP code ${otpCode} verified successfully for action ${actionType}`,
);
await tx.otp.update({
where: {
id: record.id,
},
data: {
verified_at: new Date(),
},
});

await this.prisma.otp.update({
where: {
id: record.id,
},
data: {
verified_at: new Date(),
},
return { code: 'success' };
});

return { code: 'success' };
}
}