diff --git a/api/controllers/otpController.js b/api/controllers/otpController.js index 8fde6519..d10f4a56 100644 --- a/api/controllers/otpController.js +++ b/api/controllers/otpController.js @@ -41,14 +41,16 @@ async function sendOtp(req, res) { res.status(500).json({ success: false, message: 'Server error' }); } } +const MAX_VERIFY_ATTEMPTS = 5; async function verifyOtp(req, res) { try { const { phone, otp } = req.body; + // Look up by phone only (not by otp) so failed guesses still resolve to + // a record we can track attempts against. const record = await Otp.findOne({ phone, - otp, used: false, expires_at: { $gt: new Date() }, }).sort({ created_at: -1 }); @@ -57,6 +59,21 @@ async function verifyOtp(req, res) { return res.status(400).json({ success: false, message: 'Invalid or expired OTP' }); } + if (record.attempts >= MAX_VERIFY_ATTEMPTS) { + record.used = true; // lock this OTP out; caller must request a new one + await record.save(); + return res.status(429).json({ + success: false, + message: 'Too many incorrect attempts. Please request a new OTP.', + }); + } + + if (record.otp !== otp) { + record.attempts += 1; + await record.save(); + return res.status(400).json({ success: false, message: 'Invalid or expired OTP' }); + } + record.used = true; await record.save(); diff --git a/api/index.js b/api/index.js index d233e239..6476eef4 100644 --- a/api/index.js +++ b/api/index.js @@ -88,7 +88,7 @@ function startServer(port) { }); } -if (process.env.NODE_ENV !== 'production') { +if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') { startServer(PORT); } diff --git a/api/routes/otpRoutes.js b/api/routes/otpRoutes.js index ae8e4ab7..2ab042a4 100644 --- a/api/routes/otpRoutes.js +++ b/api/routes/otpRoutes.js @@ -11,7 +11,15 @@ const otpRateLimiter = rateLimit({ message: { success: false, message: 'Too many requests from this IP, please try again after 15 minutes' }, }); +const otpVerifyRateLimiter = rateLimit({ + windowMs: 5 * 60 * 1000, // matches OTP validity window + max: 5, + standardHeaders: true, + legacyHeaders: false, + message: { success: false, message: 'Too many verification attempts, please request a new OTP' }, +}); + router.post('/send-otp', otpRateLimiter, sendOtp); -router.post('/verify-otp', verifyOtp); +router.post('/verify-otp', otpVerifyRateLimiter, verifyOtp); -module.exports = router; +module.exports = router; \ No newline at end of file diff --git a/models/Otp.js b/models/Otp.js index 5e22b170..a02f966b 100644 --- a/models/Otp.js +++ b/models/Otp.js @@ -5,6 +5,7 @@ const otpSchema = new mongoose.Schema({ otp: { type: String, required: true }, expires_at: { type: Date, required: true }, used: { type: Boolean, default: false }, + attempts: { type: Number, default: 0 }, }, { timestamps: { createdAt: 'created_at' } }); // Auto-delete OTP documents after they expire (TTL index) diff --git a/public/privacy.html b/public/privacy.html deleted file mode 100644 index e69de29b..00000000 diff --git a/public/refund.html b/public/refund.html deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/api.test.js b/tests/api.test.js index 05fbcac6..97c0a540 100644 --- a/tests/api.test.js +++ b/tests/api.test.js @@ -43,7 +43,7 @@ process.env.ADMIN_JWT_SECRET = 'secret_test_key_123'; process.env.NODE_ENV = 'test'; // Load the express app -const { app } = require('../api/index'); +const app = require('../api/index'); describe('Brownie-Bliss API Security & Endpoint Integration Tests', () => { // Clear mock history before each test