File size: 5,054 Bytes
fcf8749 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | const twilio = require('twilio');
const { generateAccessToken } = require('../config/jwt');
const accountSid = process.env.TWILIO_ACCOUNT_SID;
const authToken = process.env.TWILIO_AUTH_TOKEN;
const fromPhone = process.env.TWILIO_PHONE_NUMBER;
const IS_DEV = process.env.NODE_ENV !== 'production';
// In-memory OTP store phone β { otp, expiresAt, attempts }
const otpStore = new Map();
const OTP_TTL_MS = 5 * 60 * 1000;
const MAX_ATTEMPTS = 5;
function generateOtp() {
return Math.floor(100000 + Math.random() * 900000).toString();
}
// ββ POST /api/otp/send ββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Body: { phone: "+918925036049" } or bare 10-digit
exports.sendOtp = async (req, res) => {
const { phone } = req.body;
if (!phone || !/^\+?\d{7,15}$/.test(phone)) {
return res.status(400).json({
success: false,
error: 'Invalid phone. Use E.164 format (+918925036049) or bare 10 digits.',
});
}
// Normalise: prepend +91 for bare 10-digit numbers
const normalised = phone.startsWith('+') ? phone : `+91${phone}`;
// Rate limit
const existing = otpStore.get(normalised);
if (existing && Date.now() < existing.expiresAt && existing.attempts >= MAX_ATTEMPTS) {
const waitSec = Math.ceil((existing.expiresAt - Date.now()) / 1000);
return res.status(429).json({ success: false, error: `Too many attempts. Try again in ${waitSec}s.` });
}
const otp = generateOtp();
const expiresAt = Date.now() + OTP_TTL_MS;
otpStore.set(normalised, { otp, expiresAt, attempts: 0 });
// Try Twilio β DO NOT delete OTP on failure (keeps it usable for dev verify)
let smsSent = false;
try {
if (accountSid && authToken && fromPhone) {
const client = twilio(accountSid, authToken);
await client.messages.create({
body: `Your FairRelay code: ${otp}. Valid 5 mins. Do not share.`,
from: fromPhone,
to: normalised,
});
smsSent = true;
console.log(`[OTP] SMS sent to ${normalised.slice(0, 7)}****`);
}
} catch (err) {
console.warn('[OTP] Twilio failed (demo mode):', err.message);
}
const payload = {
success: true,
message: smsSent ? 'OTP sent to your phone.' : '[Demo] Twilio not configured β use the code below.',
expiresIn: OTP_TTL_MS / 1000,
phone: normalised,
};
// Return real OTP in dev so Flutter/Postman can autofill
if (IS_DEV && !smsSent) {
payload.demo = true;
payload.otp = otp;
} else if (!smsSent) {
// Production with Twilio not configured β don't leak the OTP
payload.message = 'OTP service unavailable. Please try again later.';
}
res.json(payload);
};
// ββ POST /api/otp/verify βββββββββββββββββββββββββββββββββββββββββββββββββ
// Body: { phone, otp, role? }
exports.verifyOtp = async (req, res) => {
const { phone, otp, role } = req.body;
if (!phone || !otp) {
return res.status(400).json({ success: false, error: 'phone and otp are required.' });
}
const normalised = phone.startsWith('+') ? phone : `+91${phone}`;
// Dev bypass: OTP '000000' always succeeds in development
const isBypass = IS_DEV && otp.toString().trim() === '000000';
if (!isBypass) {
const record = otpStore.get(normalised);
if (!record) {
return res.status(400).json({ success: false, error: 'No OTP found. Request a new one.' });
}
if (Date.now() > record.expiresAt) {
otpStore.delete(normalised);
return res.status(400).json({ success: false, error: 'OTP expired. Request a new one.' });
}
record.attempts += 1;
if (record.otp !== otp.toString().trim()) {
const remaining = MAX_ATTEMPTS - record.attempts;
if (remaining <= 0) {
otpStore.delete(normalised);
return res.status(429).json({ success: false, error: 'Too many attempts. Request a new OTP.' });
}
return res.status(400).json({ success: false, error: `Invalid OTP. ${remaining} attempt(s) remaining.` });
}
otpStore.delete(normalised);
}
console.log(`[OTP] Verified ${normalised.slice(0, 7)}**** ${isBypass ? '(dev bypass)' : ''}`);
const userRole = role || 'DRIVER';
const token = generateAccessToken(
{ phone: normalised, role: userRole, id: `dev-${normalised.slice(-4)}` }
);
res.json({
success: true,
message: 'Phone verified.',
data: {
token,
user: {
id: `dev-${normalised.slice(-4)}`,
phone: normalised,
role: userRole,
name: `Driver ${normalised.slice(-4)}`,
demo: isBypass,
},
},
});
};
// -- POST /api/otp/resend --
exports.resendOtp = async (req, res) => {
const { phone } = req.body;
if (phone) {
const normalised = phone.startsWith('+') ? phone : `+91${phone}`;
otpStore.delete(normalised);
}
return exports.sendOtp(req, res);
};
|