From bf2fd8de4f180c8e1cf7e898a81d63d158a8a88a Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Wed, 22 Oct 2025 15:00:56 +0300 Subject: [PATCH] lock otp db row --- src/api/withdrawal/withdrawal.service.ts | 29 ++++-- src/shared/global/otp.service.ts | 113 ++++++++++++----------- 2 files changed, 80 insertions(+), 62 deletions(-) diff --git a/src/api/withdrawal/withdrawal.service.ts b/src/api/withdrawal/withdrawal.service.ts index 3c5b1c3..3add106 100644 --- a/src/api/withdrawal/withdrawal.service.ts +++ b/src/api/withdrawal/withdrawal.service.ts @@ -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') { + this.logger.error( + 'Payment request denied because payment row was locked previously!', + 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; + } } } diff --git a/src/shared/global/otp.service.ts b/src/shared/global/otp.service.ts index e1256c0..2ba009a 100644 --- a/src/shared/global/otp.service.ts +++ b/src/shared/global/otp.service.ts @@ -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'; @@ -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` + 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; + `; + 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' }; } }