Auth #1
@@ -9,8 +9,12 @@
|
|||||||
|
|
||||||
// app/controllers/auth.controller.js
|
// app/controllers/auth.controller.js
|
||||||
|
|
||||||
const { checkPassword } = require("../utils/hashPassword.util");
|
const { checkPassword, hashPassword } = require("../utils/hashPassword.util");
|
||||||
const { sendMail } = require("../utils/mail.util");
|
const { sendMail } = require("../utils/mail.util");
|
||||||
|
const {
|
||||||
|
validatePassword,
|
||||||
|
} = require("../utils/validation/validatePassword.util");
|
||||||
|
const { validateEmail } = require("../utils/validation/validateEmail.util");
|
||||||
const { generateOTP, validateOTP } = require("../utils/otp.util");
|
const { generateOTP, validateOTP } = require("../utils/otp.util");
|
||||||
const { getCachedUser, clearUserCache } = require("../utils/cache.util");
|
const { getCachedUser, clearUserCache } = require("../utils/cache.util");
|
||||||
const { generateToken } = require("../utils/jwt.util");
|
const { generateToken } = require("../utils/jwt.util");
|
||||||
@@ -18,7 +22,15 @@ const {
|
|||||||
createRefreshSession,
|
createRefreshSession,
|
||||||
validateRefreshSession,
|
validateRefreshSession,
|
||||||
deleteRefreshSession,
|
deleteRefreshSession,
|
||||||
|
deleteAllUserSessions,
|
||||||
} = require("../utils/refreshSession.util");
|
} = require("../utils/refreshSession.util");
|
||||||
|
const {
|
||||||
|
createPasswordReset,
|
||||||
|
verifyPasswordResetToken,
|
||||||
|
deletePasswordReset,
|
||||||
|
sendPasswordResetEmail,
|
||||||
|
sendPasswordChangedEmail
|
||||||
|
} = require("../utils/passwordReset.utill");
|
||||||
const db = require("../models");
|
const db = require("../models");
|
||||||
const { log } = require("../utils/consoleLog.utill");
|
const { log } = require("../utils/consoleLog.utill");
|
||||||
|
|
||||||
@@ -227,6 +239,204 @@ exports.refreshToken = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.forgotPassword = async (req, res) => {
|
||||||
|
try {
|
||||||
|
let { email } = req.body;
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
return res.status(400).send({
|
||||||
|
success: false,
|
||||||
|
message: "Email is required",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
email = email.trim().toLowerCase();
|
||||||
|
|
||||||
|
if (!validateEmail(email)) {
|
||||||
|
return res.status(400).send({
|
||||||
|
success: false,
|
||||||
|
message: "Invalid email address",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await User.findOne({
|
||||||
|
where: { email },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(200).send({
|
||||||
|
success: true,
|
||||||
|
message:
|
||||||
|
"If an account exists for this email, a password reset link has been sent.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetToken = await createPasswordReset(user.id);
|
||||||
|
|
||||||
|
await sendPasswordResetEmail(user.email, user.firstName, resetToken);
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
success: true,
|
||||||
|
message:
|
||||||
|
"If an account exists for this email, a password reset link has been sent.",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("FORGOT PASSWORD ERROR:", error);
|
||||||
|
|
||||||
|
return res.status(500).send({
|
||||||
|
success: false,
|
||||||
|
message: "Unable to process password reset request",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.resetPassword = async (req, res) => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const {
|
||||||
|
token,
|
||||||
|
newPassword,
|
||||||
|
confirmPassword,
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
|
||||||
|
if (!token ||!newPassword || !confirmPassword) {
|
||||||
|
return res.status(400).send({
|
||||||
|
success: false,
|
||||||
|
message:
|
||||||
|
"Token, new password and confirm password are required",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
newPassword !==
|
||||||
|
confirmPassword
|
||||||
|
) {
|
||||||
|
return res.status(400).send({
|
||||||
|
success: false,
|
||||||
|
message:
|
||||||
|
"Passwords do not match",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (
|
||||||
|
!validatePassword(newPassword)
|
||||||
|
) {
|
||||||
|
return res.status(400).send({
|
||||||
|
success: false,
|
||||||
|
message:
|
||||||
|
"Password does not meet the required criteria",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const verification =
|
||||||
|
await verifyPasswordResetToken(
|
||||||
|
token
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
if (!verification) {
|
||||||
|
return res.status(400).send({
|
||||||
|
success: false,
|
||||||
|
message:
|
||||||
|
"Reset token is invalid or expired",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const {
|
||||||
|
userId,
|
||||||
|
redisKey,
|
||||||
|
} = verification;
|
||||||
|
|
||||||
|
const user =
|
||||||
|
await User.findByPk(userId);
|
||||||
|
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
|
||||||
|
await deletePasswordReset(
|
||||||
|
redisKey
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.status(400).send({
|
||||||
|
success: false,
|
||||||
|
message:
|
||||||
|
"Reset token is invalid or expired",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const samePassword =
|
||||||
|
await checkPassword(
|
||||||
|
newPassword,
|
||||||
|
user.password
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
if (samePassword) {
|
||||||
|
return res.status(400).send({
|
||||||
|
success: false,
|
||||||
|
message:
|
||||||
|
"New password must be different from the current password",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const hashedPassword =
|
||||||
|
await hashPassword(
|
||||||
|
newPassword
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
user.password =
|
||||||
|
hashedPassword;
|
||||||
|
|
||||||
|
user.passwordChangedAt =
|
||||||
|
new Date();
|
||||||
|
|
||||||
|
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
await sendPasswordChangedEmail(
|
||||||
|
user.email,
|
||||||
|
user.firstName
|
||||||
|
);
|
||||||
|
|
||||||
|
await deletePasswordReset(
|
||||||
|
redisKey
|
||||||
|
);
|
||||||
|
|
||||||
|
deleteAllUserSessions(
|
||||||
|
user.id
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
success: true,
|
||||||
|
message:
|
||||||
|
"Password reset successfully. Please login again.",
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
console.error(
|
||||||
|
"RESET PASSWORD ERROR:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
return res.status(500).send({
|
||||||
|
success: false,
|
||||||
|
message:
|
||||||
|
"Failed to reset password",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Logout: Clear the JWT cookie
|
// Logout: Clear the JWT cookie
|
||||||
exports.logout = (req, res) => {
|
exports.logout = (req, res) => {
|
||||||
res.clearCookie("access_token", {
|
res.clearCookie("access_token", {
|
||||||
|
|||||||
@@ -16,56 +16,60 @@ module.exports = (sequelize, DataTypes) => {
|
|||||||
id: {
|
id: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
collate: 'utf8mb4_general_ci'
|
collate: "utf8mb4_general_ci",
|
||||||
},
|
},
|
||||||
firstName: {
|
firstName: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
lastName: {
|
lastName: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true
|
unique: true,
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
},
|
||||||
accountType: {
|
accountType: {
|
||||||
type: DataTypes.ENUM("business_customer", "customer"),
|
type: DataTypes.ENUM("business_customer", "customer"),
|
||||||
defaultValue: "customer"
|
defaultValue: "customer",
|
||||||
},
|
},
|
||||||
accountStatus: {
|
accountStatus: {
|
||||||
type: DataTypes.ENUM(
|
type: DataTypes.ENUM(
|
||||||
"PENDING_VERIFICATION",
|
"PENDING_VERIFICATION",
|
||||||
"ACTIVE",
|
"ACTIVE",
|
||||||
"SUSPENDED",
|
"SUSPENDED",
|
||||||
"DEACTIVATED"
|
"DEACTIVATED",
|
||||||
),
|
),
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: "PENDING_VERIFICATION",
|
defaultValue: "PENDING_VERIFICATION",
|
||||||
},
|
},
|
||||||
emailVerifiedAt: {
|
emailVerifiedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
|
passwordChangedAt: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
defaultValue: null,
|
defaultValue: null,
|
||||||
},
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tableName: "users",
|
tableName: "users",
|
||||||
timestamps: true
|
timestamps: true,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
User.associate = (db) => {
|
User.associate = (db) => {
|
||||||
User.hasMany(db.userPermission, {
|
User.hasMany(db.userPermission, {
|
||||||
foreignKey: "user_id",
|
foreignKey: "user_id",
|
||||||
as: "userPermissions"
|
as: "userPermissions",
|
||||||
});
|
});
|
||||||
User.hasOne(db.Profile, {
|
User.hasOne(db.Profile, {
|
||||||
foreignKey: "user_id",
|
foreignKey: "user_id",
|
||||||
@@ -79,7 +83,6 @@ module.exports = (sequelize, DataTypes) => {
|
|||||||
foreignKey: "user_id",
|
foreignKey: "user_id",
|
||||||
as: "businessCustomer",
|
as: "businessCustomer",
|
||||||
});
|
});
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return User;
|
return User;
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ router.get("/me", authenticate, (req, res) => {
|
|||||||
router.post('/req-otp', authController.loginReq);
|
router.post('/req-otp', authController.loginReq);
|
||||||
router.post('/login', authController.login);
|
router.post('/login', authController.login);
|
||||||
router.post('/refresh', authController.refreshToken);
|
router.post('/refresh', authController.refreshToken);
|
||||||
|
router.post('/forgot-password', authController.forgotPassword);
|
||||||
|
router.post('/reset-password', authController.resetPassword);
|
||||||
router.post('/logout', authController.logout);
|
router.post('/logout', authController.logout);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Your ZUMRI password was changed</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body style="font-family: Arial, Helvetica, sans-serif; font-size: 14px; color: #000; line-height: 1.6; margin: 0; padding: 20px;">
|
||||||
|
|
||||||
|
<p>Hi {{customer_name}},</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Your password was changed at {{changed_at}}.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
If this was not you, contact support immediately.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Thank you,<br>
|
||||||
|
<b>ZUMRI Team</b>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="font-size: 12px; color: #555;">
|
||||||
|
Document Classification: Internal Use Only, Version: 1.0
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,53 +1,242 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Document</title>
|
<meta
|
||||||
<style>
|
name="viewport"
|
||||||
body {
|
content="width=device-width, initial-scale=1.0"
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
>
|
||||||
font-size: 14px;
|
|
||||||
color: #000;
|
<title>Reset Your ZUMRI Password</title>
|
||||||
line-height: 1.6;
|
|
||||||
margin: 0;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
width: 400px;
|
|
||||||
border: 1px solid #000;
|
|
||||||
}
|
|
||||||
th {
|
|
||||||
background-color: #053534;
|
|
||||||
color: #ffffff;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
td {
|
|
||||||
border: 1px solid #000;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: #1a73e8;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
|
||||||
<p>Dear {{firstName}},</p>
|
|
||||||
|
|
||||||
<p>Greetings from <b>Oceanic Titan</b>!</p>
|
<body
|
||||||
|
style="
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
|
||||||
<p>You have requested to reset your password for your Oceanic Titan account. Please click the link below to reset your password:</p>
|
<table
|
||||||
|
width="100%"
|
||||||
|
cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
role="presentation"
|
||||||
|
style="
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
padding: 40px 0;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
|
||||||
<p><a href="{{resetLink}}" target="_blank">Reset Password</a></p>
|
<table
|
||||||
|
width="600"
|
||||||
|
cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
role="presentation"
|
||||||
|
style="
|
||||||
|
max-width: 600px;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
|
||||||
<p>If you did not request a password reset, please ignore this email. The above link will expire in <b>{{expiryTime}}</b>.</p>
|
<!-- Greeting -->
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p
|
||||||
|
style="
|
||||||
|
font-size: 18px;
|
||||||
|
color: #333333;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Hi {{firstName}},
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<p>Regards,<br>
|
<!-- Title -->
|
||||||
<b>Oceanic Titan Team</b>
|
<tr>
|
||||||
</p>
|
<td>
|
||||||
|
<h2
|
||||||
|
style="
|
||||||
|
color: #222222;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Reset Your ZUMRI Password
|
||||||
|
</h2>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p
|
||||||
|
style="
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #555555;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
We received a request to reset the password
|
||||||
|
for your ZUMRI account.
|
||||||
|
Click the button below to create a new password.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Reset Button -->
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
align="center"
|
||||||
|
style="padding: 30px 0;"
|
||||||
|
>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="{{resetLink}}"
|
||||||
|
target="_blank"
|
||||||
|
style="
|
||||||
|
display: inline-block;
|
||||||
|
padding: 14px 28px;
|
||||||
|
background-color: #222222;
|
||||||
|
color: #ffffff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 6px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Reset Password
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Direct Link -->
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p
|
||||||
|
style="
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #777777;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
If the button doesn't work, copy and paste
|
||||||
|
the following link into your browser:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p
|
||||||
|
style="
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-break: break-all;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="{{resetLink}}"
|
||||||
|
target="_blank"
|
||||||
|
style="color: #0066cc;"
|
||||||
|
>
|
||||||
|
{{resetLink}}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Expiry Information -->
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p
|
||||||
|
style="
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #777777;
|
||||||
|
margin-top: 30px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
For security purposes, this password reset
|
||||||
|
link will expire in
|
||||||
|
<strong>{{expiryTime}}</strong>.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Security Message -->
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p
|
||||||
|
style="
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #777777;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
If you did not request a password reset,
|
||||||
|
you can safely ignore this email.
|
||||||
|
Your password will remain unchanged.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p
|
||||||
|
style="
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333333;
|
||||||
|
margin-top: 30px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Best regards,<br>
|
||||||
|
<strong>ZUMRI Team</strong>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Copyright -->
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
align="center"
|
||||||
|
style="
|
||||||
|
border-top: 1px solid #eeeeee;
|
||||||
|
padding-top: 20px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
style="
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999999;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
© {{currentYear}} ZUMRI.
|
||||||
|
All rights reserved.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
<p style="font-size: 12px; color: #555;">{{currentYear}} Oceanic Titan. This is an auto-generated email, please do not reply to this email.</p>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -10,104 +10,127 @@
|
|||||||
// app/utils/passwordReset.util.js
|
// app/utils/passwordReset.util.js
|
||||||
|
|
||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
|
const redis = require("../config/redisClient");
|
||||||
|
const { sendMail } = require("./mail.util");
|
||||||
|
|
||||||
const createRedisConnection = require("../config/redis.config");
|
const PASSWORD_RESET_TTL =
|
||||||
const redis = createRedisConnection();
|
Number(process.env.PASSWORD_RESET_TTL_SECONDS) || 900;
|
||||||
|
|
||||||
const db = require("../models");
|
const generatePasswordResetToken = () => {
|
||||||
|
return crypto.randomBytes(32).toString("hex");
|
||||||
|
};
|
||||||
|
|
||||||
const User = db.User;
|
const hashPasswordResetToken = (token) => {
|
||||||
|
return crypto.createHash("sha256").update(token).digest("hex");
|
||||||
|
};
|
||||||
|
|
||||||
const { log } = require("./consoleLog.utill");
|
const createPasswordReset = async (userId) => {
|
||||||
const { hashPassword } = require("./hashPassword.util");
|
// 1. Generate RAW token
|
||||||
|
const token = generatePasswordResetToken();
|
||||||
|
|
||||||
const RESET_TOKEN_TTL = process.env.RESET_TOKEN_TTL || 10 * 60; // 10 minutes
|
// 2. Hash RAW token
|
||||||
|
const tokenHash = hashPasswordResetToken(token);
|
||||||
|
|
||||||
/**
|
// 3. Create Redis key
|
||||||
* Generate password reset token
|
const redisKey = `password-reset:${tokenHash}`;
|
||||||
*
|
|
||||||
* @param {string} userId
|
|
||||||
* @returns {Promise<string>}
|
|
||||||
*/
|
|
||||||
async function generateResetToken(userId) {
|
|
||||||
try {
|
|
||||||
const token = crypto.randomBytes(32).toString("hex");
|
|
||||||
|
|
||||||
const tokenHash = crypto
|
// 4. Save user ID with 15 minute TTL
|
||||||
.createHash("sha256")
|
await redis.set(redisKey, userId, "EX", PASSWORD_RESET_TTL);
|
||||||
.update(token)
|
|
||||||
.digest("hex");
|
|
||||||
|
|
||||||
await redis.set(
|
// Send only RAW token to user
|
||||||
`passwordReset:user:${userId}`,
|
return token;
|
||||||
tokenHash,
|
};
|
||||||
"EX",
|
|
||||||
RESET_TOKEN_TTL
|
const verifyPasswordResetToken = async (token) => {
|
||||||
);
|
if (!token || typeof token !== "string") {
|
||||||
log(`Generated password reset token for user ${userId} with TTL of ${RESET_TOKEN_TTL} seconds, Generated token:`, token);
|
return null;
|
||||||
return token;
|
|
||||||
} catch (error) {
|
|
||||||
log("Password reset token generation failed", error);
|
|
||||||
throw new Error("Failed to generate password reset token");
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Hash token received from user
|
||||||
* Reset password using token
|
const tokenHash = hashPasswordResetToken(token);
|
||||||
*
|
|
||||||
* @param {string} userId
|
|
||||||
* @param {string} token
|
|
||||||
* @param {string} newPassword
|
|
||||||
*/
|
|
||||||
async function resetPassword(userId, token, newPassword) {
|
|
||||||
try {
|
|
||||||
const storedHash = await redis.get(
|
|
||||||
`passwordReset:user:${userId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!storedHash) {
|
// Create same Redis key
|
||||||
throw new Error("Invalid or expired reset token");
|
const redisKey = `password-reset:${tokenHash}`;
|
||||||
}
|
|
||||||
|
|
||||||
const tokenHash = crypto
|
// Search Redis
|
||||||
.createHash("sha256")
|
const userId = await redis.get(redisKey);
|
||||||
.update(token)
|
|
||||||
.digest("hex");
|
|
||||||
|
|
||||||
const isValid =
|
if (!userId) {
|
||||||
crypto.timingSafeEqual(
|
return null;
|
||||||
Buffer.from(storedHash),
|
|
||||||
Buffer.from(tokenHash)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isValid) {
|
|
||||||
throw new Error("Invalid or expired reset token");
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await User.findByPk(userId);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new Error("User not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
user.password = await hashPassword(newPassword);
|
|
||||||
|
|
||||||
await user.save();
|
|
||||||
|
|
||||||
await redis.del(`passwordReset:user:${userId}`);
|
|
||||||
|
|
||||||
log(
|
|
||||||
`Password reset completed successfully for user ${userId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
log("Password reset failed", error);
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return {
|
||||||
|
userId,
|
||||||
|
redisKey,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const deletePasswordReset = async (redisKey) => {
|
||||||
|
await redis.del(redisKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendPasswordResetEmail =
|
||||||
|
async (email, firstName, resetToken) => {
|
||||||
|
|
||||||
|
const resetLink =
|
||||||
|
`${process.env.FRONTEND_URL}/reset-password?token=${resetToken}`;
|
||||||
|
|
||||||
|
|
||||||
|
await sendMail({
|
||||||
|
to: email,
|
||||||
|
|
||||||
|
subject:
|
||||||
|
"Reset your ZUMRI password",
|
||||||
|
|
||||||
|
templateName:
|
||||||
|
"passwordReset",
|
||||||
|
|
||||||
|
templateVars: {
|
||||||
|
firstName: firstName,
|
||||||
|
resetLink: resetLink,
|
||||||
|
expiryTime: "15 minutes",
|
||||||
|
},
|
||||||
|
|
||||||
|
text:
|
||||||
|
`Hi ${firstName}, use this link within 15 minutes to reset your password: ${resetLink}`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendPasswordChangedEmail = async (
|
||||||
|
email,
|
||||||
|
firstName
|
||||||
|
) => {
|
||||||
|
|
||||||
|
const changedAt =
|
||||||
|
new Date().toLocaleString();
|
||||||
|
|
||||||
|
await sendMail({
|
||||||
|
to: email,
|
||||||
|
|
||||||
|
subject:
|
||||||
|
"Your ZUMRI password was changed",
|
||||||
|
|
||||||
|
templateName:
|
||||||
|
"passwordChanged",
|
||||||
|
|
||||||
|
templateVars: {
|
||||||
|
customer_name:
|
||||||
|
firstName,
|
||||||
|
|
||||||
|
changed_at:
|
||||||
|
changedAt,
|
||||||
|
},
|
||||||
|
|
||||||
|
text:
|
||||||
|
`Hi ${firstName}, your password was changed at ${changedAt}. If this was not you, contact support immediately.`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
generateResetToken,
|
createPasswordReset,
|
||||||
resetPassword
|
verifyPasswordResetToken,
|
||||||
|
deletePasswordReset,
|
||||||
|
hashPasswordResetToken,
|
||||||
|
sendPasswordResetEmail,
|
||||||
|
sendPasswordChangedEmail,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user