add create user and login user parts
This commit is contained in:
@@ -14,10 +14,18 @@ const { sendMail } = require("../utils/mail.util");
|
||||
const { generateOTP, validateOTP } = require("../utils/otp.util");
|
||||
const { getCachedUser, clearUserCache } = require("../utils/cache.util");
|
||||
const { generateToken } = require("../utils/jwt.util");
|
||||
const {
|
||||
createRefreshSession,
|
||||
validateRefreshSession,
|
||||
deleteRefreshSession,
|
||||
} = require("../utils/refreshSession.util");
|
||||
const db = require("../models");
|
||||
const { log } = require("../utils/consoleLog.utill");
|
||||
|
||||
const appName = process.env.APP_NAME || "Niolla";
|
||||
|
||||
const User = db.User;
|
||||
|
||||
// Login Step 1: Request OTP
|
||||
exports.loginReq = async (req, res) => {
|
||||
try {
|
||||
@@ -65,6 +73,12 @@ exports.login = async (req, res) => {
|
||||
try {
|
||||
const { email, otp } = req.body;
|
||||
|
||||
if (!email || !otp) {
|
||||
return res
|
||||
.status(400)
|
||||
.send({ success: false, message: "Email and OTP are required" });
|
||||
}
|
||||
|
||||
const user = await getCachedUser(email);
|
||||
|
||||
if (!user) {
|
||||
@@ -73,7 +87,14 @@ exports.login = async (req, res) => {
|
||||
.send({ success: false, message: "User Not Found" });
|
||||
}
|
||||
|
||||
const isValidOTP = validateOTP(email, otp);
|
||||
if (user.accountStatus !== "ACTIVE") {
|
||||
return res.status(403).send({
|
||||
success: false,
|
||||
message: "Account is not active",
|
||||
});
|
||||
}
|
||||
|
||||
const isValidOTP = validateOTP(email, String(otp));
|
||||
|
||||
if (!isValidOTP) {
|
||||
return res
|
||||
@@ -87,16 +108,24 @@ exports.login = async (req, res) => {
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
accountType: user.accountType,
|
||||
});
|
||||
|
||||
const { refreshToken } = createRefreshSession(user.id);
|
||||
|
||||
// 3. Set JWT as HttpOnly cookie
|
||||
res.cookie("access_token", token, {
|
||||
httpOnly: true, // JS cannot access
|
||||
secure: process.env.NODE_ENV === "production", // HTTPS only in prod
|
||||
sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
|
||||
maxAge: 24 * 60 * 60 * 1000, // 1 day
|
||||
maxAge: 15 * 60 * 1000, // 1 day
|
||||
});
|
||||
|
||||
res.cookie("refresh_token", refreshToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
|
||||
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
|
||||
});
|
||||
|
||||
log(`JWT issued for ${email}`);
|
||||
@@ -112,6 +141,7 @@ exports.login = async (req, res) => {
|
||||
lastName: user.lastName,
|
||||
role: user.role,
|
||||
accountType: user.accountType,
|
||||
accessToken: token,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -120,6 +150,83 @@ exports.login = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
exports.refreshToken = async (req, res) => {
|
||||
try {
|
||||
const refreshToken = req.cookies?.refresh_token;
|
||||
|
||||
if (!refreshToken) {
|
||||
return res.status(401).send({
|
||||
success: false,
|
||||
message: "Refresh token is required",
|
||||
});
|
||||
}
|
||||
|
||||
const session = validateRefreshSession(refreshToken);
|
||||
|
||||
if (!session) {
|
||||
res.clearCookie("refresh_token");
|
||||
|
||||
return res.status(401).send({
|
||||
success: false,
|
||||
message: "Invalid or expired session. Please login again.",
|
||||
});
|
||||
}
|
||||
|
||||
const user = await User.findByPk(session.userId);
|
||||
|
||||
if (!user || user.accountStatus !== "ACTIVE") {
|
||||
deleteRefreshSession(session.sessionId);
|
||||
|
||||
return res.status(401).send({
|
||||
success: false,
|
||||
message: "Session is no longer valid",
|
||||
});
|
||||
}
|
||||
|
||||
// Generate new Access Token
|
||||
const token = generateToken({
|
||||
id: user.id,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email: user.email,
|
||||
accountType: user.accountType,
|
||||
});
|
||||
|
||||
// Generate new Refresh Token
|
||||
const { refreshToken: newRefreshToken } = createRefreshSession(user.id);
|
||||
|
||||
deleteRefreshSession(session.sessionId);
|
||||
|
||||
// Replace access cookie
|
||||
res.cookie("access_token", token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
|
||||
maxAge: 15 * 60 * 1000,
|
||||
});
|
||||
|
||||
// Replace refresh cookie
|
||||
res.cookie("refresh_token", newRefreshToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
|
||||
maxAge: 7 * 24 * 60 * 60 * 1000,
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
success: true,
|
||||
message: "Session refreshed successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("REFRESH ERROR:", error);
|
||||
|
||||
return res.status(401).send({
|
||||
success: false,
|
||||
message: "Invalid or expired session. Please login again.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Logout: Clear the JWT cookie
|
||||
exports.logout = (req, res) => {
|
||||
res.clearCookie("access_token", {
|
||||
|
||||
+103
-180
@@ -9,19 +9,29 @@
|
||||
|
||||
// app/controllers/user.controller.js
|
||||
|
||||
// const { Op } = require("sequelize");
|
||||
const db = require("../models");
|
||||
const { hashPassword } = require("../utils/hashPassword.util");
|
||||
const { validatePassword } = require("../utils/validation/validatePassword.util");
|
||||
const {
|
||||
validatePassword,
|
||||
} = require("../utils/validation/validatePassword.util");
|
||||
const { validateEmail } = require("../utils/validation/validateEmail.util");
|
||||
const { generateUserId, generateId } = require("../utils/idGen.util");
|
||||
const {
|
||||
createCustomerDetails,
|
||||
} = require("../utils/users/createCustomerDetails.util");
|
||||
const {
|
||||
createBusinessCustomerDetails,
|
||||
} = require("../utils/users/createBusinessCustomer.util");
|
||||
const { logActivity } = require("../services/activity.service");
|
||||
const { sendMail } = require("../utils/mail.util");
|
||||
const {
|
||||
createEmailVerification, verifyEmailVerificationToken, deleteEmailVerification} = require("../utils/emailVerification.util");;
|
||||
createEmailVerification,
|
||||
verifyEmailVerificationToken,
|
||||
sendVerificationEmail,
|
||||
deleteEmailVerification,
|
||||
} = require("../utils/emailVerification.util");
|
||||
const User = db.User;
|
||||
const Profile = db.Profile;
|
||||
// const EmailVerification = db.EmailVerification;
|
||||
|
||||
// Create a new user
|
||||
exports.createNewUser = async (req, res) => {
|
||||
@@ -33,15 +43,35 @@ exports.createNewUser = async (req, res) => {
|
||||
lastName,
|
||||
email,
|
||||
password,
|
||||
accountType,
|
||||
address,
|
||||
phoneNumber,
|
||||
businessName,
|
||||
businessRegistrationNumber,
|
||||
businessType,
|
||||
contactName,
|
||||
businessEmail,
|
||||
expectedMonthlyVolume,
|
||||
note,
|
||||
} = req.body;
|
||||
|
||||
if (!firstName || !lastName || !email || !password) {
|
||||
if (!firstName || !lastName || !email || !password || !accountType) {
|
||||
await transaction.rollback();
|
||||
|
||||
return res.status(400).send({
|
||||
success: false,
|
||||
message:
|
||||
"First name, last name, email and password are required",
|
||||
"First name, last name, email, password and account type are required",
|
||||
});
|
||||
}
|
||||
|
||||
if (accountType !== "customer" && accountType !== "business_customer") {
|
||||
await transaction.rollback();
|
||||
|
||||
return res.status(400).send({
|
||||
success: false,
|
||||
message:
|
||||
"Invalid account type. Must be either 'customer' or 'business_customer'",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -66,14 +96,6 @@ if (!emailValid) {
|
||||
});
|
||||
}
|
||||
|
||||
// let pass;
|
||||
|
||||
// if(accountType === "admin" || accountType === "superadmin" || accountType === "manager" || accountType === "support_agent") {
|
||||
// pass = process.env.DEFAULT_PASSWORD;
|
||||
// }else{
|
||||
// pass = password;
|
||||
// }
|
||||
|
||||
const validatePasswordResult = validatePassword(password);
|
||||
|
||||
if (!validatePasswordResult) {
|
||||
@@ -95,14 +117,13 @@ if (!emailValid) {
|
||||
lastName,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
// accountType,
|
||||
accountType,
|
||||
accountStatus: "PENDING_VERIFICATION",
|
||||
emailVerifiedAt: null,
|
||||
},
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
|
||||
const newProfile = await Profile.create(
|
||||
{
|
||||
profile_id: generateId(),
|
||||
@@ -117,78 +138,52 @@ if (!emailValid) {
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
//create customer and business customer
|
||||
if (accountType === "customer") {
|
||||
const customerData = {
|
||||
address,phoneNumber,
|
||||
};
|
||||
|
||||
await createCustomerDetails(
|
||||
newUser.id,
|
||||
customerData,
|
||||
transaction,
|
||||
);
|
||||
} else if (accountType === "business_customer") {
|
||||
const businessData = {
|
||||
businessName,
|
||||
businessRegistrationNumber,
|
||||
businessType,
|
||||
contactName,
|
||||
phoneNumber,
|
||||
businessEmail,
|
||||
expectedMonthlyVolume,
|
||||
note,
|
||||
};
|
||||
|
||||
await createBusinessCustomerDetails(
|
||||
newUser.id,
|
||||
businessData,
|
||||
transaction,
|
||||
);
|
||||
}
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
const verificationToken = await createEmailVerification(newUser.id);
|
||||
|
||||
const confirmationLink =
|
||||
`${process.env.FRONTEND_URL}/verify-email?token=${verificationToken}`;
|
||||
|
||||
|
||||
try {
|
||||
|
||||
await sendMail({
|
||||
to:
|
||||
email,
|
||||
|
||||
subject:
|
||||
"Confirm Your ZUMRI Account",
|
||||
|
||||
templateName:
|
||||
"emailVerification",
|
||||
|
||||
templateVars: {
|
||||
|
||||
customer_name:
|
||||
firstName,
|
||||
|
||||
confirmation_link:
|
||||
confirmationLink,
|
||||
},
|
||||
|
||||
text:
|
||||
`Hello ${firstName}, please verify your ZUMRI account using this link: ${confirmationLink}`,
|
||||
});
|
||||
|
||||
|
||||
} catch (mailError) {
|
||||
|
||||
console.error(
|
||||
"VERIFICATION EMAIL ERROR:",
|
||||
mailError
|
||||
);
|
||||
|
||||
|
||||
return res.status(201).send({
|
||||
success: true,
|
||||
|
||||
message:
|
||||
"Account created, but verification email could not be sent. Please request a new verification email.",
|
||||
|
||||
data: {
|
||||
id:
|
||||
newUser.id,
|
||||
|
||||
firstName:
|
||||
newUser.firstName,
|
||||
|
||||
lastName:
|
||||
newUser.lastName,
|
||||
|
||||
email:
|
||||
await sendVerificationEmail(
|
||||
newUser.email,
|
||||
|
||||
accountType:
|
||||
newUser.accountType,
|
||||
|
||||
accountStatus:
|
||||
newUser.accountStatus,
|
||||
},
|
||||
});
|
||||
newUser.firstName,
|
||||
verificationToken,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error sending verification email:", error);
|
||||
}
|
||||
|
||||
await logActivity({
|
||||
user: req.user,
|
||||
user: newUser,
|
||||
description: `Created New User with ID: ${newUser.id}`,
|
||||
type: "CREATE_USER",
|
||||
module: "User Management",
|
||||
@@ -202,9 +197,8 @@ if (!emailValid) {
|
||||
firstName: newUser.firstName,
|
||||
lastName: newUser.lastName,
|
||||
email: newUser.email,
|
||||
// accountType: newUser.accountType,
|
||||
// role: newUser.role,
|
||||
// department: newUser.department,
|
||||
accountType: newUser.accountType,
|
||||
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -212,10 +206,7 @@ if (!emailValid) {
|
||||
await transaction.rollback();
|
||||
}
|
||||
|
||||
console.error(
|
||||
"CREATE USER ERROR:",
|
||||
error
|
||||
);
|
||||
console.error("CREATE USER ERROR:", error);
|
||||
|
||||
res.status(500).send({
|
||||
success: false,
|
||||
@@ -226,122 +217,72 @@ if (!emailValid) {
|
||||
};
|
||||
|
||||
// Verify customer email
|
||||
exports.verifyEmail =
|
||||
async (req, res) => {
|
||||
|
||||
const transaction =
|
||||
await db.sequelize.transaction();
|
||||
|
||||
exports.verifyEmail = async (req, res) => {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
|
||||
try {
|
||||
const {
|
||||
token,
|
||||
} = req.body;
|
||||
|
||||
const { token } = req.body;
|
||||
|
||||
if (!token) {
|
||||
|
||||
await transaction.rollback();
|
||||
|
||||
|
||||
return res.status(400).send({
|
||||
success:
|
||||
false,
|
||||
success: false,
|
||||
|
||||
message:
|
||||
"Verification token is required",
|
||||
message: "Verification token is required",
|
||||
});
|
||||
}
|
||||
|
||||
const verification =
|
||||
await verifyEmailVerificationToken(
|
||||
token
|
||||
);
|
||||
const verification = await verifyEmailVerificationToken(token);
|
||||
|
||||
if (!verification) {
|
||||
|
||||
await transaction.rollback();
|
||||
|
||||
|
||||
return res.status(400).send({
|
||||
success:
|
||||
false,
|
||||
success: false,
|
||||
|
||||
message:
|
||||
"Verification token is invalid or expired",
|
||||
message: "Verification token is invalid or expired",
|
||||
});
|
||||
}
|
||||
|
||||
const { userId, redisKey } = verification;
|
||||
|
||||
|
||||
const {
|
||||
userId,
|
||||
redisKey,
|
||||
} =
|
||||
verification;
|
||||
|
||||
const user =
|
||||
await User.findOne({
|
||||
const user = await User.findOne({
|
||||
where: {
|
||||
id:
|
||||
userId,
|
||||
id: userId,
|
||||
},
|
||||
|
||||
transaction,
|
||||
});
|
||||
|
||||
|
||||
|
||||
if (!user) {
|
||||
|
||||
await transaction.rollback();
|
||||
|
||||
await deleteEmailVerification(
|
||||
redisKey
|
||||
);
|
||||
|
||||
await deleteEmailVerification(redisKey);
|
||||
|
||||
return res.status(404).send({
|
||||
success:
|
||||
false,
|
||||
success: false,
|
||||
|
||||
message:
|
||||
"User not found",
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
user.accountStatus ===
|
||||
"ACTIVE" &&
|
||||
user.emailVerifiedAt
|
||||
) {
|
||||
|
||||
if (user.accountStatus === "ACTIVE" && user.emailVerifiedAt) {
|
||||
await transaction.rollback();
|
||||
|
||||
|
||||
// Token is no longer needed
|
||||
await deleteEmailVerification(
|
||||
redisKey
|
||||
);
|
||||
|
||||
await deleteEmailVerification(redisKey);
|
||||
|
||||
return res.status(400).send({
|
||||
success:
|
||||
false,
|
||||
success: false,
|
||||
|
||||
message:
|
||||
"Email is already verified",
|
||||
message: "Email is already verified",
|
||||
});
|
||||
}
|
||||
|
||||
user.accountStatus = "ACTIVE";
|
||||
|
||||
user.accountStatus =
|
||||
"ACTIVE";
|
||||
|
||||
|
||||
user.emailVerifiedAt =
|
||||
new Date();
|
||||
|
||||
user.emailVerifiedAt = new Date();
|
||||
|
||||
await user.save({
|
||||
transaction,
|
||||
@@ -349,42 +290,24 @@ exports.verifyEmail =
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
await deleteEmailVerification(
|
||||
redisKey
|
||||
);
|
||||
|
||||
|
||||
await deleteEmailVerification(redisKey);
|
||||
|
||||
return res.status(200).send({
|
||||
success:
|
||||
true,
|
||||
success: true,
|
||||
|
||||
message:
|
||||
"Email verified successfully. Your account is now active.",
|
||||
message: "Email verified successfully. Your account is now active.",
|
||||
});
|
||||
|
||||
|
||||
} catch (error) {
|
||||
|
||||
if (!transaction.finished) {
|
||||
|
||||
await transaction.rollback();
|
||||
|
||||
}
|
||||
|
||||
|
||||
console.error(
|
||||
"VERIFY EMAIL ERROR:",
|
||||
error
|
||||
);
|
||||
|
||||
console.error("VERIFY EMAIL ERROR:", error);
|
||||
|
||||
return res.status(500).send({
|
||||
success:
|
||||
false,
|
||||
success: false,
|
||||
|
||||
message:
|
||||
"Failed to verify email",
|
||||
message: "Failed to verify email",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -41,6 +41,8 @@ db.sequelize = sequelize;
|
||||
|
||||
// User and Authentication
|
||||
db.User = require("./user/user.model")(sequelize, DataTypes);
|
||||
db.Customer = require("./user/customer.model")(sequelize, DataTypes);
|
||||
db.BusinessCustomer = require("./user/businessCustomer.model")(sequelize, DataTypes);
|
||||
db.UserActivity = require("./activities/userActivities.model")(sequelize, DataTypes);
|
||||
db.Profile = require("./user/profile.model")(sequelize, DataTypes);
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
//app/models/user/businessCustomer.model.js
|
||||
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const BusinessCustomer = sequelize.define(
|
||||
"BusinessCustomer",
|
||||
{
|
||||
business_customer_id: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true,
|
||||
},
|
||||
|
||||
user_id: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
},
|
||||
|
||||
businessName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
businessRegistrationNumber: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
businessType: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
contactName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
phoneNumber: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
businessEmail: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
expectedMonthlyVolume: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
note: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
tableName: "business_customers",
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
|
||||
BusinessCustomer.associate = (db) => {
|
||||
BusinessCustomer.belongsTo(db.User, {
|
||||
foreignKey: "user_id",
|
||||
as: "user",
|
||||
});
|
||||
};
|
||||
|
||||
return BusinessCustomer;
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is proprietary and confidential.
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file, via any medium, is strictly prohibited.
|
||||
*/
|
||||
|
||||
// app/models/customer.model.js
|
||||
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const Customer = sequelize.define(
|
||||
"Customer",
|
||||
{
|
||||
customer_id: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true,
|
||||
collate: "utf8mb4_general_ci",
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
},
|
||||
address: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
phoneNumber: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
tableName: "customers",
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
|
||||
Customer.associate = (db) => {
|
||||
Customer.belongsTo(db.User, {
|
||||
foreignKey: "user_id",
|
||||
as: "user",
|
||||
});
|
||||
};
|
||||
|
||||
return Customer;
|
||||
};
|
||||
@@ -36,7 +36,7 @@ module.exports = (sequelize, DataTypes) => {
|
||||
allowNull: false
|
||||
},
|
||||
accountType: {
|
||||
type: DataTypes.ENUM("admin", "superadmin", "manager", "business_customer", "rider", "customer","support_agent"),
|
||||
type: DataTypes.ENUM("business_customer", "customer"),
|
||||
defaultValue: "customer"
|
||||
},
|
||||
accountStatus: {
|
||||
@@ -71,6 +71,14 @@ module.exports = (sequelize, DataTypes) => {
|
||||
foreignKey: "user_id",
|
||||
as: "profile",
|
||||
});
|
||||
User.hasOne(db.Customer, {
|
||||
foreignKey: "user_id",
|
||||
as: "customer",
|
||||
});
|
||||
User.hasOne(db.BusinessCustomer, {
|
||||
foreignKey: "user_id",
|
||||
as: "businessCustomer",
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ router.get("/me", authenticate, (req, res) => {
|
||||
|
||||
router.post('/req-otp', authController.loginReq);
|
||||
router.post('/login', authController.login);
|
||||
router.post('/refresh', authController.refreshToken);
|
||||
router.post('/logout', authController.logout);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>GLAURA 2FA Code</title>
|
||||
<title>ZUMRI CEYLON</title>
|
||||
</head>
|
||||
<body style="font-family: Arial, Helvetica, sans-serif; font-size: 14px; color: #000; line-height: 1.6; margin: 0; padding: 20px;">
|
||||
<p>Dear {{firstName}},</p>
|
||||
|
||||
<p>Greetings from <b>Oceanic Titan</b>!</p>
|
||||
<p>Greetings from <b>ZUMRI CEYLON</b>!</p>
|
||||
|
||||
<p>Your One Time Password (OTP) to log in to your Oceanic Titan account is mentioned below.</p>
|
||||
<p>Your One Time Password (OTP) to log in to your ZUMRI CEYLON account is mentioned below.</p>
|
||||
|
||||
<p>Please enter this OTP in the required field to proceed further:</p>
|
||||
|
||||
@@ -24,14 +24,14 @@
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p>The above-mentioned OTP is valid for <b>5 minutes</b>.</p>
|
||||
<p>The above-mentioned OTP is valid for <b>15 minutes</b>.</p>
|
||||
|
||||
<br>
|
||||
|
||||
<p>Regards,<br>
|
||||
<b>Oceanic Titan Team</b>
|
||||
<b>ZUMRI CEYLON Team</b>
|
||||
</p>
|
||||
|
||||
<p style="font-size: 12px; color: #555;">{{currentYear}} Oceanic Titan. This is an auto-generated email, please do not reply to this email.</p>
|
||||
<p style="font-size: 12px; color: #555;">{{currentYear}} ZUMRI CEYLON. This is an auto-generated email, please do not reply to this email.</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,103 +1,91 @@
|
||||
// app/utils/emailVerification.util.js
|
||||
|
||||
const crypto = require("crypto");
|
||||
|
||||
const { sendMail } = require("../utils/mail.util");
|
||||
const redis = require("../config/redisClient");
|
||||
|
||||
const EMAIL_VERIFICATION_TTL =
|
||||
Number(
|
||||
process.env.EMAIL_VERIFICATION_TTL_SECONDS
|
||||
) || 1800;
|
||||
Number(process.env.EMAIL_VERIFICATION_TTL_SECONDS) || 1800;
|
||||
|
||||
const generateEmailVerificationToken = () => {
|
||||
return crypto
|
||||
.randomBytes(32)
|
||||
.toString("hex");
|
||||
return crypto.randomBytes(32).toString("hex");
|
||||
};
|
||||
|
||||
const hashEmailVerificationToken = (token) => {
|
||||
return crypto
|
||||
.createHash("sha256")
|
||||
.update(token)
|
||||
.digest("hex");
|
||||
return crypto.createHash("sha256").update(token).digest("hex");
|
||||
};
|
||||
|
||||
const createEmailVerification = async (userId) => {
|
||||
|
||||
// 1. Generate raw token
|
||||
const token =
|
||||
generateEmailVerificationToken();
|
||||
|
||||
const token = generateEmailVerificationToken();
|
||||
|
||||
// 2. Hash token
|
||||
const tokenHash =
|
||||
hashEmailVerificationToken(token);
|
||||
|
||||
const tokenHash = hashEmailVerificationToken(token);
|
||||
|
||||
// 3. Create Redis key
|
||||
const redisKey =
|
||||
`email-verification:${tokenHash}`;
|
||||
const redisKey = `email-verification:${tokenHash}`;
|
||||
|
||||
await redis.set(
|
||||
redisKey,
|
||||
userId,
|
||||
"EX",
|
||||
EMAIL_VERIFICATION_TTL
|
||||
);
|
||||
await redis.set(redisKey, userId, "EX", EMAIL_VERIFICATION_TTL);
|
||||
|
||||
return token;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Check a verification token.
|
||||
*/
|
||||
const verifyEmailVerificationToken =
|
||||
async (token) => {
|
||||
|
||||
if (
|
||||
!token ||
|
||||
typeof token !== "string"
|
||||
) {
|
||||
const verifyEmailVerificationToken = async (token) => {
|
||||
if (!token || typeof token !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// 1. Hash token received from user
|
||||
const tokenHash =
|
||||
hashEmailVerificationToken(token);
|
||||
|
||||
const tokenHash = hashEmailVerificationToken(token);
|
||||
|
||||
// 2. Build same Redis key
|
||||
const redisKey =
|
||||
`email-verification:${tokenHash}`;
|
||||
|
||||
const redisKey = `email-verification:${tokenHash}`;
|
||||
|
||||
// 3. Search Redis
|
||||
const userId =
|
||||
await redis.get(redisKey);
|
||||
const userId = await redis.get(redisKey);
|
||||
|
||||
if (!userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
userId,
|
||||
redisKey,
|
||||
};
|
||||
};
|
||||
|
||||
const deleteEmailVerification =
|
||||
async (redisKey) => {
|
||||
//send verification email to user
|
||||
const sendVerificationEmail = async (email, firstName, verificationToken) => {
|
||||
const confirmationLink = `${process.env.FRONTEND_URL}/verify-email?token=${verificationToken}`;
|
||||
|
||||
await redis.del(redisKey);
|
||||
// Send email
|
||||
await sendMail({
|
||||
to: email,
|
||||
|
||||
subject: "Confirm Your ZUMRI Account",
|
||||
|
||||
templateName: "emailVerification",
|
||||
|
||||
templateVars: {
|
||||
customer_name: firstName,
|
||||
confirmation_link: confirmationLink,
|
||||
},
|
||||
|
||||
text: `Hello ${firstName}, please verify your ZUMRI account using this link: ${confirmationLink}`,
|
||||
});
|
||||
};
|
||||
|
||||
const deleteEmailVerification = async (redisKey) => {
|
||||
await redis.del(redisKey);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createEmailVerification,
|
||||
verifyEmailVerificationToken,
|
||||
sendVerificationEmail,
|
||||
deleteEmailVerification,
|
||||
hashEmailVerificationToken,
|
||||
};
|
||||
@@ -23,6 +23,14 @@ const generateUserId = () => {
|
||||
return "usr_" + Math.random().toString(36).slice(2, 10);
|
||||
};
|
||||
|
||||
const generateCustomerId = () => {
|
||||
return "cust_" + Math.random().toString(36).slice(2, 10);
|
||||
}
|
||||
|
||||
const generateBusinessCustomerId = () => {
|
||||
return "b_cust_" + Math.random().toString(36).slice(2, 10);
|
||||
}
|
||||
|
||||
const generateClientId = () => {
|
||||
const prefix = "cli_";
|
||||
return prefix + uuidv4();
|
||||
@@ -141,6 +149,8 @@ const generateDocumentReferenceNo = async (type) => {
|
||||
|
||||
module.exports = {
|
||||
generateUserId,
|
||||
generateCustomerId,
|
||||
generateBusinessCustomerId,
|
||||
generateClientId,
|
||||
generateInquId,
|
||||
generateInquRefNo,
|
||||
|
||||
+12
-1
@@ -15,6 +15,9 @@ require("dotenv").config();
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "your_jwt_secret_key";
|
||||
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "1d"; // token validity
|
||||
|
||||
const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET || "your_refresh_token_secret_key";
|
||||
const REFRESH_TOKEN_DAYS = process.env.REFRESH_TOKEN_DAYS || "7d"; // refresh token validity
|
||||
|
||||
/**
|
||||
* Generate JWT token
|
||||
* @param {Object} payload - usually { id, email, role }
|
||||
@@ -33,4 +36,12 @@ const verifyToken = (token) => {
|
||||
return jwt.verify(token, JWT_SECRET);
|
||||
};
|
||||
|
||||
module.exports = { generateToken, verifyToken };
|
||||
const generateRefreshToken = (payload) => {
|
||||
return jwt.sign(payload, REFRESH_TOKEN_SECRET, { expiresIn: REFRESH_TOKEN_DAYS });
|
||||
}
|
||||
|
||||
const verifyRefreshToken = (token) => {
|
||||
return jwt.verify(token, REFRESH_TOKEN_SECRET);
|
||||
}
|
||||
|
||||
module.exports = { generateToken, verifyToken, generateRefreshToken, verifyRefreshToken };
|
||||
|
||||
@@ -17,8 +17,9 @@ function generateOTP(key){
|
||||
return otp;
|
||||
}
|
||||
|
||||
function saveOTP(key, otp, ttl = 5 * 60 * 1000) { // default TTL: 5 mins
|
||||
const expiresAt = Date.now() + ttl;
|
||||
function saveOTP(key, otp) {
|
||||
ttl = parseInt(process.env.LOGIN_OTP_TTL_SECONDS || 300);
|
||||
const expiresAt = Date.now() + ttl * 1000; // Convert seconds to milliseconds
|
||||
otpCache.set(key, { otp, expiresAt });
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
// app/utils/refreshSession.util.js
|
||||
|
||||
const crypto = require("crypto");
|
||||
|
||||
const {
|
||||
generateRefreshToken,
|
||||
verifyRefreshToken,
|
||||
} = require("./jwt.util");
|
||||
|
||||
const refreshSessions = new Map();
|
||||
|
||||
|
||||
// Default = 7 days
|
||||
const REFRESH_SESSION_TTL =
|
||||
7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
|
||||
const hashRefreshToken = (token) => {
|
||||
return crypto
|
||||
.createHash("sha256")
|
||||
.update(token)
|
||||
.digest("hex");
|
||||
};
|
||||
|
||||
const createRefreshSession = (userId) => {
|
||||
|
||||
// Unique session ID
|
||||
const sessionId =
|
||||
crypto.randomUUID();
|
||||
|
||||
|
||||
// Create raw Refresh JWT
|
||||
const refreshToken =
|
||||
generateRefreshToken({
|
||||
sub: userId,
|
||||
sid: sessionId,
|
||||
});
|
||||
|
||||
|
||||
// Hash raw refresh token
|
||||
const tokenHash =
|
||||
hashRefreshToken(
|
||||
refreshToken
|
||||
);
|
||||
|
||||
|
||||
// Expiration time
|
||||
const expiresAt =
|
||||
Date.now() +
|
||||
REFRESH_SESSION_TTL;
|
||||
|
||||
|
||||
// Save only HASH in RAM
|
||||
refreshSessions.set(
|
||||
sessionId,
|
||||
{
|
||||
userId,
|
||||
tokenHash,
|
||||
expiresAt,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
return {
|
||||
refreshToken,
|
||||
sessionId,
|
||||
};
|
||||
};
|
||||
|
||||
const validateRefreshSession = (
|
||||
refreshToken
|
||||
) => {
|
||||
|
||||
if (!refreshToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
let decoded;
|
||||
|
||||
try {
|
||||
|
||||
decoded =
|
||||
verifyRefreshToken(
|
||||
refreshToken
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
|
||||
const sessionId =
|
||||
decoded.sid;
|
||||
|
||||
const userId =
|
||||
decoded.sub;
|
||||
|
||||
|
||||
if (!sessionId || !userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// Get session from RAM
|
||||
const session =
|
||||
refreshSessions.get(
|
||||
sessionId
|
||||
);
|
||||
|
||||
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// Check session expiration
|
||||
if (
|
||||
Date.now() >
|
||||
session.expiresAt
|
||||
) {
|
||||
|
||||
refreshSessions.delete(
|
||||
sessionId
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// Hash received refresh token
|
||||
const receivedHash =
|
||||
hashRefreshToken(
|
||||
refreshToken
|
||||
);
|
||||
|
||||
|
||||
// Compare stored hash
|
||||
if (
|
||||
receivedHash !==
|
||||
session.tokenHash
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// Extra user check
|
||||
if (
|
||||
session.userId !==
|
||||
userId
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
userId,
|
||||
sessionId,
|
||||
};
|
||||
};
|
||||
|
||||
const deleteRefreshSession = (
|
||||
sessionId
|
||||
) => {
|
||||
|
||||
refreshSessions.delete(
|
||||
sessionId
|
||||
);
|
||||
};
|
||||
|
||||
const deleteAllUserSessions = (
|
||||
userId
|
||||
) => {
|
||||
|
||||
for (
|
||||
const [sessionId, session]
|
||||
of refreshSessions.entries()
|
||||
) {
|
||||
|
||||
if (
|
||||
session.userId === userId
|
||||
) {
|
||||
|
||||
refreshSessions.delete(
|
||||
sessionId
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
module.exports = {
|
||||
createRefreshSession,
|
||||
validateRefreshSession,
|
||||
deleteRefreshSession,
|
||||
deleteAllUserSessions,
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
// app/utils/users/createBusinessCustomer.util.js
|
||||
|
||||
const db = require("../../models");
|
||||
|
||||
const {
|
||||
generateBusinessCustomerId,
|
||||
} = require("../idGen.util");
|
||||
|
||||
const BusinessCustomer = db.BusinessCustomer;
|
||||
|
||||
|
||||
/**
|
||||
* Create business-customer-specific details
|
||||
*/
|
||||
const createBusinessCustomerDetails = async (
|
||||
userId,
|
||||
businessData,
|
||||
transaction
|
||||
) => {
|
||||
|
||||
const {
|
||||
businessName,
|
||||
businessRegistrationNumber,
|
||||
businessType,
|
||||
contactName,
|
||||
phoneNumber,
|
||||
businessEmail,
|
||||
expectedMonthlyVolume,
|
||||
note,
|
||||
} = businessData;
|
||||
|
||||
|
||||
if (
|
||||
!businessName ||
|
||||
!businessRegistrationNumber ||
|
||||
!businessType ||
|
||||
!contactName ||
|
||||
!phoneNumber ||
|
||||
!businessEmail ||
|
||||
!expectedMonthlyVolume
|
||||
) {
|
||||
throw new Error(
|
||||
"Required business customer details are missing"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const businessCustomer =
|
||||
await BusinessCustomer.create(
|
||||
{
|
||||
business_customer_id:
|
||||
generateBusinessCustomerId(),
|
||||
|
||||
user_id:
|
||||
userId,
|
||||
|
||||
businessName,
|
||||
|
||||
businessRegistrationNumber,
|
||||
|
||||
businessType,
|
||||
|
||||
contactName,
|
||||
|
||||
phoneNumber,
|
||||
|
||||
businessEmail,
|
||||
|
||||
expectedMonthlyVolume,
|
||||
|
||||
note:
|
||||
note || null,
|
||||
},
|
||||
{
|
||||
transaction,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
return businessCustomer;
|
||||
};
|
||||
|
||||
|
||||
module.exports = {
|
||||
createBusinessCustomerDetails,
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
//app/utils/users/createCustomerDetails.util.js
|
||||
|
||||
const db = require("../../models");
|
||||
|
||||
const { generateCustomerId } = require("../idGen.util");
|
||||
|
||||
const Customer = db.Customer;
|
||||
|
||||
const createCustomerDetails = async (
|
||||
userId,
|
||||
customerData,
|
||||
transaction
|
||||
) => {
|
||||
|
||||
const {
|
||||
address,
|
||||
phoneNumber,
|
||||
} = customerData;
|
||||
|
||||
|
||||
if (!address || !phoneNumber) {
|
||||
throw new Error(
|
||||
"Address and phone number are required for customer"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const customer = await Customer.create(
|
||||
{
|
||||
customer_id:
|
||||
generateCustomerId(),
|
||||
|
||||
user_id:
|
||||
userId,
|
||||
|
||||
address:
|
||||
address,
|
||||
|
||||
phoneNumber:
|
||||
phoneNumber,
|
||||
},
|
||||
{
|
||||
transaction,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
return customer;
|
||||
};
|
||||
|
||||
|
||||
module.exports = {
|
||||
createCustomerDetails,
|
||||
};
|
||||
Reference in New Issue
Block a user