| const prisma = require('../config/database'); |
| const { generateAccessToken, refreshAccessToken } = require('../config/jwt'); |
| const Joi = require('joi'); |
| const twilio = require('twilio'); |
| const crypto = require('crypto'); |
| const dotenv = require('dotenv'); |
| dotenv.config(); |
|
|
| |
| const otpStore = new Map(); |
| const OTP_TTL = 5 * 60 * 1000; |
| const MAX_ATT = 3; |
|
|
| function makeTwilioClient() { |
| try { |
| return twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN); |
| } catch { return null; } |
| } |
|
|
| |
| |
| |
| const sendOTP = async (req, res) => { |
| try { |
| const schema = Joi.object({ |
| phone: Joi.string().pattern(/^\+?\d{7,15}$/).required().messages({ |
| 'string.pattern.base': 'Phone must be a valid number (e.g. +91XXXXXXXXXX)', |
| }), |
| role: Joi.string().valid('DRIVER', 'SHIPPER', 'DISPATCHER').default('DRIVER'), |
| }); |
| const { error } = schema.validate(req.body); |
| if (error) return res.status(400).json({ success: false, message: error.details[0].message }); |
|
|
| const { phone } = req.body; |
|
|
| |
| const existing = otpStore.get(phone); |
| if (existing && Date.now() < existing.expiresAt && existing.attempts >= MAX_ATT) { |
| const wait = Math.ceil((existing.expiresAt - Date.now()) / 1000); |
| return res.status(429).json({ success: false, message: `Too many attempts. Retry in ${wait}s.` }); |
| } |
|
|
| const otp = Math.floor(100000 + Math.random() * 900000).toString(); |
| otpStore.set(phone, { otp, expiresAt: Date.now() + OTP_TTL, attempts: 0 }); |
|
|
| |
| const client = makeTwilioClient(); |
| let smsSent = false; |
| if (client && process.env.TWILIO_PHONE_NUMBER) { |
| try { |
| await client.messages.create({ |
| body: `Your FairRelay login code is: ${otp}. Valid for 5 minutes. Do not share.`, |
| from: process.env.TWILIO_PHONE_NUMBER, |
| to: phone, |
| }); |
| smsSent = true; |
| console.log(`[OTP] SMS sent to ${phone.slice(0, 5)}***`); |
| } catch (twilioErr) { |
| console.warn('[OTP] Twilio send failed (demo mode):', twilioErr.message); |
| } |
| } |
|
|
| const IS_PROD = process.env.NODE_ENV === 'production'; |
| res.status(200).json({ |
| success: true, |
| message: smsSent ? 'OTP sent to your phone.' : (IS_PROD ? 'OTP service unavailable. Please try again later.' : `Demo mode — OTP is: ${otp}`), |
| data: { phone, demo: !smsSent, otp: (!IS_PROD && !smsSent) ? otp : undefined }, |
| }); |
| } catch (err) { |
| console.error('sendOTP error:', err.message); |
| res.status(500).json({ success: false, message: err.message || 'Failed to send OTP' }); |
| } |
| }; |
|
|
| |
| |
| |
| const verifyOTP = async (req, res) => { |
| try { |
| const schema = Joi.object({ |
| phone: Joi.string().pattern(/^\+?\d{7,15}$/).required(), |
| otp: Joi.string().length(6).required(), |
| role: Joi.string().valid('DRIVER', 'SHIPPER', 'DISPATCHER').optional(), |
| }); |
| const { error } = schema.validate(req.body); |
| if (error) return res.status(400).json({ success: false, message: error.details[0].message }); |
|
|
| const { phone, otp, role } = req.body; |
|
|
| |
| const record = otpStore.get(phone); |
| if (!record) { |
| return res.status(400).json({ success: false, message: 'No OTP found. Request a new one.' }); |
| } |
| if (Date.now() > record.expiresAt) { |
| otpStore.delete(phone); |
| return res.status(400).json({ success: false, message: 'OTP expired. Request a new one.' }); |
| } |
| record.attempts += 1; |
| if (record.otp !== otp.trim()) { |
| if (record.attempts >= MAX_ATT) { |
| otpStore.delete(phone); |
| return res.status(429).json({ success: false, message: 'Too many failed attempts. Request a new OTP.' }); |
| } |
| return res.status(400).json({ success: false, message: `Invalid OTP. ${MAX_ATT - record.attempts} attempt(s) left.` }); |
| } |
| otpStore.delete(phone); |
|
|
| |
| let user; |
| let isDemo = false; |
| try { |
| user = await prisma.user.findUnique({ |
| where: { phone }, |
| include: { trucks: { select: { id: true, licensePlate: true, model: true } } }, |
| }); |
| if (!user) { |
| user = await prisma.user.create({ |
| data: { phone, role: role || 'DISPATCHER', name: `User_${phone.slice(-4)}` }, |
| include: { trucks: true }, |
| }); |
| } |
| await prisma.user.update({ where: { id: user.id }, data: { lastActiveDate: new Date() } }); |
| } catch (dbErr) { |
| console.warn('[Auth] DB offline — using demo user:', dbErr.message); |
| isDemo = true; |
| user = { |
| id: `demo-${crypto.randomUUID()}`, |
| name: `Dispatcher_${phone.slice(-4)}`, |
| phone, |
| role: role || 'DISPATCHER', |
| status: 'ACTIVE', |
| rating: 5.0, |
| deliveriesCount: 0, |
| totalEarnings: 0, |
| weeklyEarnings: 0, |
| trucks: [], |
| }; |
| } |
|
|
| const token = generateAccessToken({ userId: user.id, role: user.role }); |
|
|
| res.status(200).json({ |
| success: true, |
| message: isDemo ? 'Login successful (demo mode)' : 'Login successful', |
| data: { |
| token, |
| user: { |
| id: user.id, |
| name: user.name, |
| phone: user.phone, |
| role: user.role, |
| status: user.status, |
| rating: user.rating, |
| deliveriesCount: user.deliveriesCount, |
| totalEarnings: user.totalEarnings, |
| weeklyEarnings: user.weeklyEarnings, |
| trucks: user.trucks || [], |
| }, |
| }, |
| }); |
| } catch (err) { |
| console.error('verifyOTP error:', err.message); |
| res.status(500).json({ success: false, message: err.message || 'OTP verification failed' }); |
| } |
| }; |
|
|
| |
| |
| |
| const getProfile = async (req, res) => { |
| try { |
| const userId = req.user.id; |
| const user = await prisma.user.findUnique({ |
| where: { id: userId }, |
| include: { |
| trucks: { select: { id: true, licensePlate: true, model: true, capacity: true, currentLat: true, currentLng: true } }, |
| transactions: { take: 5, orderBy: { createdAt: 'desc' }, select: { id: true, amount: true, type: true, description: true, route: true, createdAt: true } }, |
| }, |
| }); |
| if (!user) return res.status(404).json({ success: false, message: 'User not found' }); |
|
|
| res.status(200).json({ |
| success: true, |
| data: { |
| id: user.id, name: user.name, phone: user.phone, role: user.role, |
| status: user.status, rating: user.rating, deliveriesCount: user.deliveriesCount, |
| totalEarnings: user.totalEarnings, weeklyEarnings: user.weeklyEarnings, |
| weeklyKmDriven: user.weeklyKmDriven, trucks: user.trucks || [], |
| recentTransactions: user.transactions || [], lastActiveDate: user.lastActiveDate, |
| }, |
| }); |
| } catch (err) { |
| console.error('getProfile error:', err.message); |
| res.status(500).json({ success: false, message: 'Failed to fetch profile' }); |
| } |
| }; |
|
|
| |
| |
| |
| const refreshToken = async (req, res) => { |
| try { |
| const { error } = Joi.object({ refreshToken: Joi.string().required().label('Refresh token') }).validate(req.body); |
| if (error) return res.status(400).json({ success: false, message: error.details[0].message }); |
|
|
| const newAccessToken = refreshAccessToken(req.body.refreshToken); |
| res.status(200).json({ success: true, message: 'Token refreshed successfully', data: { accessToken: newAccessToken } }); |
| } catch (err) { |
| console.error('refreshToken error:', err.message); |
| if (err.message === 'Invalid refresh token') { |
| return res.status(403).json({ success: false, message: 'Invalid or expired refresh token' }); |
| } |
| res.status(500).json({ success: false, message: 'Token refresh failed' }); |
| } |
| }; |
|
|
| module.exports = { sendOTP, verifyOTP, getProfile, refreshToken }; |
|
|