const crypto = require('crypto'); const express = require('express'); const jwt = require('jsonwebtoken'); const prisma = require('../db'); const { asyncHandler, httpError } = require('../lib/errors'); const { isValidPhone, normalizePhone } = require('../lib/phone'); const { publicUser, userPublicSelect } = require('../lib/users'); const { assignedDealerIds } = require('../lib/access'); const { sendSmsOtp } = require('../services/smsService'); const { requireAuth } = require('../middleware/auth'); const router = express.Router(); const OTP_TTL_MS = 5 * 60 * 1000; function generateOtp() { return String(crypto.randomInt(0, 1000000)).padStart(6, '0'); } async function accessibleDealers(user) { if (user.role === 'superAdmin') { return prisma.dealer.findMany({ orderBy: { name: 'asc' } }); } if (user.role === 'dealer') { if (!user.dealerId) return []; const dealer = await prisma.dealer.findUnique({ where: { id: user.dealerId } }); return dealer ? [dealer] : []; } const ids = assignedDealerIds(user); if (ids.length === 0) return []; return prisma.dealer.findMany({ where: { id: { in: ids } }, orderBy: { name: 'asc' }, }); } router.post( '/send-otp', asyncHandler(async (req, res) => { const phone = normalizePhone(req.body?.phone); if (!isValidPhone(phone)) { throw httpError(400, 'Phone must be in 947XXXXXXXX format'); } const user = await prisma.user.findUnique({ where: { phone } }); if (!user) { return res.status(404).json({ success: false, message: 'Not Registered' }); } const otp = generateOtp(); const otpExpiresAt = new Date(Date.now() + OTP_TTL_MS); await prisma.user.update({ where: { id: user.id }, data: { currentOtp: otp, otpExpiresAt }, }); try { await sendSmsOtp(phone, otp); } catch (error) { await prisma.user.update({ where: { id: user.id }, data: { currentOtp: null, otpExpiresAt: null }, }); throw httpError(502, error.message || 'Failed to send OTP SMS'); } return res.json({ success: true, message: 'OTP sent via SMS' }); }) ); router.post( '/verify-otp', asyncHandler(async (req, res) => { const phone = normalizePhone(req.body?.phone); const otp = String(req.body?.otp || '').trim(); if (!isValidPhone(phone) || !/^\d{6}$/.test(otp)) { throw httpError(400, 'Phone and a 6-digit OTP are required'); } const user = await prisma.user.findUnique({ where: { phone }, select: { ...userPublicSelect, currentOtp: true, otpExpiresAt: true, }, }); if (!user || !user.currentOtp || !user.otpExpiresAt) { throw httpError(401, 'Invalid OTP'); } if (user.otpExpiresAt.getTime() < Date.now()) { throw httpError(401, 'OTP expired'); } const stored = Buffer.from(user.currentOtp); const given = Buffer.from(otp); const matches = stored.length === given.length && crypto.timingSafeEqual(stored, given); if (!matches) { throw httpError(401, 'Invalid OTP'); } await prisma.user.update({ where: { id: user.id }, data: { currentOtp: null, otpExpiresAt: null }, }); const token = jwt.sign({ sub: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: '10y', }); const dealers = await accessibleDealers(user); return res.json({ token, user: publicUser(user, dealers) }); }) ); router.get( '/me', requireAuth, asyncHandler(async (req, res) => { const dealers = await accessibleDealers(req.user); return res.json(publicUser(req.user, dealers)); }) ); module.exports = router;