diff --git a/api/main_endpoints/routes/MembershipPayment.js b/api/main_endpoints/routes/MembershipPayment.js index 808e8aa87..994555934 100644 --- a/api/main_endpoints/routes/MembershipPayment.js +++ b/api/main_endpoints/routes/MembershipPayment.js @@ -7,7 +7,8 @@ const { SERVER_ERROR, NOT_FOUND, OK, - UNAUTHORIZED + UNAUTHORIZED, + TOO_MANY_REQUESTS } = require('../../util/constants').STATUS_CODES; const membershipState = require('../../util/constants').MEMBERSHIP_STATE; const { updateMembershipExpiration } = require('../util/userHelpers'); @@ -21,6 +22,10 @@ const logger = require('../../util/logger'); const AuditLogActions = require('../util/auditLogActions'); const AuditLog = require('../models/AuditLog'); +const attemptCount = new Map(); +const MAX_ATTEMPTS = 5; + + router.post('/verifyMembership', async (req, res) => { const decoded = await decodeToken(req, membershipState.PENDING); if (decoded.status !== OK) { @@ -29,6 +34,14 @@ router.post('/verifyMembership', async (req, res) => { const { confirmationCode } = req.body; const userId = decoded.token._id; + const attempts = attemptCount.get(userId) ?? 0; + + if (attempts >= MAX_ATTEMPTS){ + logger.error(`User ${userId} has made too many verification attempts.`); + return res.status(TOO_MANY_REQUESTS).json({ + remainingAttempts: 0 + }); + } if (!confirmationCode) { logger.error('Confirmation code missing from verifyMembership request'); @@ -37,8 +50,11 @@ router.post('/verifyMembership', async (req, res) => { const paymentDocument = await findVerifyPayment(confirmationCode, userId); if (!paymentDocument) { + attemptCount.set(userId, attempts + 1); logger.error('Error verifying payment for user:', userId); - return res.status(NOT_FOUND).send('Error verifying payment.'); + return res.status(NOT_FOUND).json({ + remainingAttempts: MAX_ATTEMPTS - (attempts + 1) + }); } const { amount } = paymentDocument; @@ -53,6 +69,8 @@ router.post('/verifyMembership', async (req, res) => { logger.error('Error updating membership expiration for user:', decoded.token._id); return res.status(SERVER_ERROR).send('Error updating membership expiration.'); } + + attemptCount.delete(userId); logger.info('Membership verified and updated for user:', decoded.token._id); AuditLog.create({ userId: decoded.token._id, diff --git a/api/util/constants.js b/api/util/constants.js index 7b7e6b68a..c2fad1bb3 100644 --- a/api/util/constants.js +++ b/api/util/constants.js @@ -5,6 +5,7 @@ const STATUS_CODES = { FORBIDDEN: 403, NOT_FOUND: 404, CONFLICT: 409, + TOO_MANY_REQUESTS: 429, SERVER_ERROR: 500, }; diff --git a/src/APIFunctions/MembershipPayment.js b/src/APIFunctions/MembershipPayment.js index 88efed2ad..648d17a05 100644 --- a/src/APIFunctions/MembershipPayment.js +++ b/src/APIFunctions/MembershipPayment.js @@ -1,5 +1,6 @@ import { ApiResponse } from './ApiResponses'; import { BASE_API_URL } from '../Enums'; +const TOO_MANY_ATTEMPTS = 429; export async function verifyMembershipFromDb(token, confirmationCode) { let status = new ApiResponse(); @@ -13,8 +14,13 @@ export async function verifyMembershipFromDb(token, confirmationCode) { }, body: JSON.stringify({ confirmationCode }) }); - status.responseData = res.status; - status.error = !res.ok; + if (res.status == TOO_MANY_ATTEMPTS) { + const data = await res.json(); + status.responseData = data; + status.error = false; + } else { + status.error = !res.ok; + } } catch (err) { status.error = true; status.responseData = err; diff --git a/src/Pages/Profile/MemberView/VerifyMembershipModal.js b/src/Pages/Profile/MemberView/VerifyMembershipModal.js index 638fb78b4..b0e85b549 100644 --- a/src/Pages/Profile/MemberView/VerifyMembershipModal.js +++ b/src/Pages/Profile/MemberView/VerifyMembershipModal.js @@ -15,6 +15,10 @@ export default function VerifyMembershipModal(props) { user.token, confirmationCode, ); + if (apiResponse.remainingAttempts !== undefined){ + bannerCallback(`Wrong Code - You have ${apiResponse.remainingAttempts} left.`); + return; + } if (apiResponse.error) { bannerCallback(`Unable to verify membership. Please try again later. Status Code: ${apiResponse.responseData || 500}`, 'red'); return;