Skip to content
Open
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
19 changes: 18 additions & 1 deletion api/controllers/otpController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -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();

Expand Down
2 changes: 1 addition & 1 deletion api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
12 changes: 10 additions & 2 deletions api/routes/otpRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
1 change: 1 addition & 0 deletions models/Otp.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Empty file removed public/privacy.html
Empty file.
Empty file removed public/refund.html
Empty file.
2 changes: 1 addition & 1 deletion tests/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down