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 = [
+2 -1
View File
@@ -6,7 +6,8 @@
// const databaseURL = 'mongodb://ostanmri_user:admin1234@localhost:27017/ostanmri_database'
const databaseURL =
"mongodb://root:f2d63313cc3ed6ff@srv-captain--ostandari-db:27017/ostan_db?authSource=admin";
// "mongodb://root:f2d63313cc3ed6ff@srv-captain--ostandari-db:27017/ostan_db?authSource=admin";
"mongodb://root:f2d63313cc3ed6ff@danak-db-public.danakcorp.com:50123/ostan_db?authSource=admin";
//const databaseURL = 'mongodb://danakcorp:danakcorp%212024@ostan-mr.ir:27017/ostan_db?serverSelectionTimeoutMS=5000&connectTimeoutMS=10000&authSource=admin&authMechanism=SCRAM-SHA-1'
// const databaseURL = 'mongodb://ostanmri_user:admin1234@91.98.96.174:27017/ostanmri_database?serverSelectionTimeoutMS=5000&connectTimeoutMS=10000&authSource=ostanmri_database'
+4 -7
View File
@@ -8,7 +8,7 @@ const {
isLoggedIn,
hasPermission,
} = require("./authentication");
const bodyParser = require("body-parser");
// const bodyParser = require("body-parser");
const cronJobs = require("./plugins/cronJobs");
const swaggerJsdoc = require("swagger-jsdoc");
const cors = require("cors");
@@ -66,10 +66,11 @@ const options = {
apis: ["./server/routes/*.js"],
};
// const specs = swaggerJsdoc(options);
const specs = swaggerJsdoc(options);
cronJobs();
createAdmins();
const corsOptions = {
origin: "*",
credentials: true,
@@ -81,11 +82,7 @@ app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(fileUpload());
app.use(cors(corsOptions));
// app.use(
// "/v1",
// swaggerUi.serve,
// swaggerUi.setup(specs , {explorer:true})
// );
app.use("/v1", swaggerUi.serve, swaggerUi.setup(specs, { explorer: true }));
// Require & Import API routes
const Public = require("./routes/public");
const admin = require("./routes/admin");
+5 -5
View File
@@ -1,11 +1,11 @@
const mongoose = require('mongoose');
const mongoosePaginate = require('mongoose-paginate-v2');
const mongoose = require("mongoose");
const mongoosePaginate = require("mongoose-paginate-v2");
const LoggerSchema = mongoose.Schema({
LastLogin: String,
user :{type: mongoose.Schema.Types.ObjectId, ref: 'User'},
LastLogin: String,
user: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
});
LoggerSchema.plugin(mongoosePaginate);
module.exports = mongoose.model('Logger', LoggerSchema);
module.exports = mongoose.model("Logger", LoggerSchema);
+4
View File
@@ -0,0 +1,4 @@
const NodeCache = require("node-cache");
class CacheService extends NodeCache {}
module.exports = new CacheService();
+12
View File
@@ -0,0 +1,12 @@
exports.paginationOptions = ({ page, limit, sort }) => {
const parsedPage = parseInt(page, 10) || 1;
const parsedLimit = parseInt(limit, 10) || 10;
const parsedSort = sort === "asc" ? "asc" : "desc";
return {
page: parsedPage,
limit: parsedLimit,
sort: parsedSort,
skip: (parsedPage - 1) * parsedLimit,
};
};
File diff suppressed because it is too large Load Diff
+47
View File
@@ -0,0 +1,47 @@
const axios = require("axios");
module.exports.sms = async (phoneNumber, message) => {
try {
// get token
const getToken = await axios.post("https://RestfulSms.com/api/Token", {
UserApiKey: "569ee43ed0d9ac27708ce43d",
SecretKey: "drtime@A!@#",
});
if (!getToken.data.IsSuccessful) {
throw new Error("sending sms failed");
}
const token = getToken.data.TokenKey;
//get lines
const resData = await axios.get("https://RestfulSms.com/api/SMSLine", {
headers: {
"Content-Type": "application/json",
"x-sms-ir-secure-token": token,
},
});
// message details
const data = {
Messages: [message],
MobileNumbers: [phoneNumber],
LineNumber: resData.data.SMSLines[0].LineNumber,
SendDateTime: "",
CanContinueInCaseOfError: "false",
};
const response = await axios.post(
`http://RestfulSms.com/api/MessageSend`,
data,
{
headers: {
"Content-Type": "application/json",
"x-sms-ir-secure-token": token,
},
}
);
return response.data;
} catch (e) {
console.log(e);
throw e;
}
};
+220 -151
View File
File diff suppressed because it is too large Load Diff
+89 -12
View File
@@ -1,7 +1,7 @@
const {Router} = require('express')
const router = Router()
const userController = require('../controllers/userController');
const electionController = require('../controllers/electionController');
const { Router } = require("express");
const router = Router();
const userController = require("../controllers/userController");
const electionController = require("../controllers/electionController");
/**
* @swagger
@@ -48,7 +48,84 @@ const electionController = require('../controllers/electionController');
* description: Validation error. Indicates that the request body contains invalid data.
*/
// Login User
router.post('/login', userController.login);
router.post("/login", userController.login);
/**
* @swagger
* /auth/otp:
* post:
* summary: Send OTP to user for login
* description: Allows users to log in with their phone number by sending an OTP.
* tags:
* - Auth
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* phone:
* type: string
* example:
* phone: "09124569875"
* responses:
* 200:
* description: OTP sent successfully.
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* example:
* message: "OTP sent successfully."
*/
router.post("/otp", userController.otp);
/**
* @swagger
* /auth/otp/verify:
* post:
* summary: Verify the OTP sent to user
* description: Allows users to log in by verifying the OTP sent to their phone number.
* tags:
* - Auth
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* phone:
* type: string
* code:
* type: string
* example:
* phone: "09124569875"
* code: "12345"
* responses:
* 200:
* description: User logged in successfully.
* content:
* application/json:
* schema:
* type: object
* properties:
* token:
* type: string
* example:
* token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
* 401:
* description: Unauthorized. Indicates that the provided credentials are invalid.
* 403:
* description: Forbidden. Indicates that the account is inactive. Contact the admin for assistance.
* 422:
* description: Validation error. Indicates that the request body contains invalid data.
*/
router.post("/otp/verify", userController.verifyOtp);
/**
* @swagger
* /auth/logout:
@@ -73,7 +150,7 @@ router.post('/login', userController.login);
* description: Unauthorized. Indicates that the user is not logged in.
*/
// Logout User
router.delete('/logout', userController.logout);
router.delete("/logout", userController.logout);
/**
* @swagger
* /auth/user:
@@ -96,7 +173,7 @@ router.delete('/logout', userController.logout);
* description: Unauthorized. Indicates that the user is not authenticated or the token is invalid.
*/
// Data User
router.get('/user', userController.getuser);
router.get("/user", userController.getuser);
/**
* @swagger
* /auth/forget-password:
@@ -154,7 +231,7 @@ router.get('/user', userController.getuser);
* description: Error message indicating the internal server error.
*/
// Change Password
router.post('/changePassword' , userController.forgetPassGenerateToken);
router.post("/changePassword", userController.forgetPassGenerateToken);
/**
* @swagger
* /auth/changePassword/{token}:
@@ -213,7 +290,7 @@ router.post('/changePassword' , userController.forgetPassGenerateToken);
* description: Error message indicating the internal server error.
*/
// Change Password :token param
router.put('/changePassword/:token' , userController.forgetPassGetToken);
router.put("/changePassword/:token", userController.forgetPassGetToken);
///////// voter
@@ -272,7 +349,7 @@ router.put('/changePassword/:token' , userController.forgetPassGetToken);
*/
//Voter Login
router.post('/voter/login' , electionController.voterLogin);
router.post("/voter/login", electionController.voterLogin);
/**
* @swagger
* /auth/voter/user:
@@ -316,6 +393,6 @@ router.post('/voter/login' , electionController.voterLogin);
* description: Error message indicating the internal server error.
*/
// user Voter Data
router.get('/voter/user' , electionController.getVoterUser);
router.get("/voter/user", electionController.getVoterUser);
module.exports = router
module.exports = router;