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);
};