chore: add otp based login

This commit is contained in:
mahyargdz
2024-11-18 09:16:17 +03:30
parent 245051024b
commit d887cf67c1
16 changed files with 1266 additions and 713 deletions
+81
View File
@@ -0,0 +1,81 @@
const { validationResult, query } = require("express-validator");
const Logger = require("../models/Logger");
const { paginationOptions } = require("../plugins/pagination");
// get admin login logs with optional filtering and pagination
module.exports.getAdminLoginLogs = [
[
query("page")
.optional()
.isInt({ min: 1 })
.withMessage("Page must be a positive integer."),
query("limit")
.optional()
.isInt({ min: 1 })
.withMessage("Limit must be a positive integer."),
query("sort")
.optional()
.isIn(["asc", "desc"])
.withMessage('Sort must be either "asc" or "desc".'),
query("fromDate")
.optional()
.isISO8601()
.withMessage("From date must be a valid date."),
query("toDate")
.optional()
.isISO8601()
.withMessage("To date must be a valid date."),
],
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty())
return res.status(422).json({ validation: errors.mapped() });
try {
const {
page = 1,
limit = 1000,
sort = "desc",
fromDate,
toDate,
} = req?.query;
const filters = {};
if (fromDate) filters.LastLogin = { $gte: new Date(fromDate) };
if (toDate)
filters.LastLogin = { ...filters.LastLogin, $lte: new Date(toDate) };
const options = paginationOptions({ page, limit, sort });
const logs = await Logger.find(filters)
.populate({ path: "user", select: { password: 0 } })
.sort({ updated_at: sort === "desc" ? -1 : 1 })
.skip(options.skip)
.limit(options.limit);
const totalLogs = await Logger.countDocuments(filters);
const totalPages = Math.ceil(totalLogs / options.limit);
return res.status(200).json({
logs: logs.map((log) => {
log.LastLogin = new Date(log.LastLogin).toLocaleString("fa-IR", {
timeZone: "Asia/Tehran",
// hour: "2-digit",
// minute: "2-digit",
// day: "2-digit",
// month: "2-digit",
// weekday: "short",
// year: "2-digit",
// timeZoneName: "long",
});
return log;
}),
page: options.page,
totalPages,
totalLogs,
});
} catch (error) {
console.error("Error fetching admin login logs:", error);
return res.status(500).json({ message: "Server error occurred." });
}
},
];
+176
View File
@@ -1,5 +1,6 @@
const User = require("../models/User");
const { body, validationResult } = require("express-validator");
const { rateLimit } = require("express-rate-limit");
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const crypto = require("crypto");
@@ -17,6 +18,9 @@ const fs = require("fs");
const nodemailer = require("nodemailer");
const Category = require("../models/Category");
const Logger = require("../models/Logger");
const cacheService = require("../plugins/cache");
const { sms } = require("../plugins/sms");
////// variables
const profileWidth = 500;
const profileHeight = 500;
@@ -25,11 +29,19 @@ const hostURL = "localhost:6688";
const limit = 20;
const select = "-password -token";
const limiter = rateLimit({
windowMs: 10 * 60 * 1000, // 10 minutes
limit: 10,
standardHeaders: "draft-7",
legacyHeaders: false,
});
/** @todo add private to query */
/** auth --------------------------------------------- */
/////////////////////////////////////////////////////////////////////// login
module.exports.login = [
limiter,
[
body("username")
.if((value, { req }) => {
@@ -138,6 +150,170 @@ module.exports.login = [
}
},
];
/** otp --------------------------------------------- */
/////////////////////////////////////////////////////////////////////// otp
module.exports.otp = [
limiter,
[
body("phone")
.notEmpty()
.withMessage(_sr["fa"].required.phone_number)
.bail()
.isMobilePhone("fa-IR")
.withMessage(_sr["fa"].required.phone_number),
// .isLength({ min: 4 })
// .withMessage(_sr["fa"].min_char.min4),
],
async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty())
return res.status(422).json({ validation: errors.mapped() });
const { phone } = req.body;
const user = await User.findOne({ mobileNumber: phone });
if (!user)
return res.status(406).json({
message: "شماره موبایل نامعتبر می باشد",
});
const existOtp = cacheService.get(`OTP:LOGIN:${phone}`);
if (existOtp)
return res.status(400).json({
message: "کد به شماره موبایل شما ارسال شده است",
ttl: (cacheService.getTtl(`OTP:LOGIN:${phone}`) - Date.now()) / 1000,
});
const otp = crypto.randomInt(10000, 99999);
const message = `کد ورود شما ${otp} میباشد. استانداری لغو11`;
/** save otp in cache */
cacheService.set(`OTP:LOGIN:${phone}`, otp, 120);
/** send sms to user */
const smsResult = await sms(phone, message);
if (!smsResult.IsSuccessful) {
console.log(smsResult);
return res500(res, `مشکلی در ارسال پیامک پیش آمده است`);
}
return res.status(200).json({
message: "کد به شماره موبایل شما ارسال شد",
});
} catch (error) {
return res500(res, error?.message);
}
},
];
/////////////////////////////////////////////////////////////////////// verify otp
module.exports.verifyOtp = [
limiter,
[
body("phone")
.notEmpty()
.withMessage(_sr["fa"].required.phone_number)
.bail()
.isMobilePhone("fa-IR")
.withMessage(_sr["fa"].required.phone_number),
body("code")
.notEmpty()
.withMessage(_sr["fa"].required.otp_code)
.bail()
.isLength({ min: 5, max: 5 })
.withMessage(_sr["fa"].format.otp_length),
],
async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty())
return res.status(422).json({ validation: errors.mapped() });
const { phone, code } = req.body;
// Check if the OTP exists in the cache
const cachedOtp = cacheService.get(`OTP:LOGIN:${phone}`);
if (!cachedOtp) {
return res.status(400).json({
message: "کد وارد شده منقضی شده است یا نامعتبر می‌باشد",
});
}
// Verify if the provided OTP matches the cached OTP
if (cachedOtp.toString() !== code.trim()) {
return res.status(422).json({
validation: { code: { msg: "کد وارد شده نامعتبر می‌باشد" } },
});
}
// Find the user by phone number
const user = await User.findOne({ mobileNumber: phone });
if (!user) {
return res.status(406).json({
message: "شماره موبایل نامعتبر می باشد",
});
}
// Check if the user's registration is complete
if (!user.registration_done) {
return res.status(403).json({
message: `اکانت شما غیرفعال می باشد لطفا با ادمین تماس بگیرید`,
});
}
// Create or update login logger
const logger = await Logger.findOne({ user: user._id });
if (logger) {
await Logger.findOneAndUpdate(
{ user: user._id },
{ $set: { LastLogin: new Date() } },
{ new: true }
);
} else {
const newLogger = new Logger({
user: user,
LastLogin: new Date(),
});
await newLogger.save();
}
// Check if the user already has a valid token
if (user.token) {
jwt.verify(
user.token.replace(/^Bearer\s/, ""),
secretKey,
async (err, decoded) => {
if (err || !decoded) {
// Generate a new token if the existing one is invalid
const token = await jwt.sign({ _id: user._id }, secretKey, {
expiresIn: "2h",
});
user.token = token;
await user.save();
// Remove the OTP from the cache
cacheService.del(`OTP:LOGIN:${phone}`);
return res.json({ token: user.token });
} else {
// Remove the OTP from the cache
cacheService.del(`OTP:LOGIN:${phone}`);
return res.json({ token: user.token });
}
}
);
} else {
// Generate a new token if none exists
const token = await jwt.sign({ _id: user._id }, secretKey, {
expiresIn: "1h",
});
user.token = token;
await user.save();
// Remove the OTP from the cache
cacheService.del(`OTP:LOGIN:${phone}`);
return res.json({ token: user.token });
}
} catch (error) {
return res500(res, error?.message);
}
},
];
/////////////////////////////////////////////////////////////////////// logout
module.exports.logout = [