FairRelay / ops /backend-dm /controllers /authController.js
MouleeswaranM's picture
Upload folder using huggingface_hub
fcf8749 verified
raw
history blame
8.2 kB
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();
// In-memory OTP store: phone → { otp, expiresAt, attempts }
const otpStore = new Map();
const OTP_TTL = 5 * 60 * 1000; // 5 minutes
const MAX_ATT = 3;
function makeTwilioClient() {
try {
return twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);
} catch { return null; }
}
/**
* POST /api/auth/login — Send OTP via Twilio SMS
*/
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;
// Rate limit: block if previous OTP not expired & too many attempts
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 });
// Try Twilio — fall back gracefully in demo mode
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' });
}
};
/**
* POST /api/auth/verify-otp — Verify OTP and issue JWT
*/
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;
// Validate OTP from in-memory store
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); // ✅ OTP matched — clean up
// Find or create user — with full DB offline fallback
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' });
}
};
/**
* GET /api/auth/profile — Get authenticated user profile (PROTECTED)
*/
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' });
}
};
/**
* POST /api/auth/refresh-token
*/
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 };