Files
senaro_dealer_api/src/routes/auth.js
T
2026-10-01 11:52:17 +05:30

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;