124 lines
3.6 KiB
JavaScript
124 lines
3.6 KiB
JavaScript
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;
|