Implement password reset and change features with email notifications
This commit is contained in:
@@ -10,104 +10,127 @@
|
||||
// app/utils/passwordReset.util.js
|
||||
|
||||
const crypto = require("crypto");
|
||||
const redis = require("../config/redisClient");
|
||||
const { sendMail } = require("./mail.util");
|
||||
|
||||
const createRedisConnection = require("../config/redis.config");
|
||||
const redis = createRedisConnection();
|
||||
const PASSWORD_RESET_TTL =
|
||||
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 { hashPassword } = require("./hashPassword.util");
|
||||
const createPasswordReset = async (userId) => {
|
||||
// 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);
|
||||
|
||||
/**
|
||||
* Generate password reset token
|
||||
*
|
||||
* @param {string} userId
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function generateResetToken(userId) {
|
||||
try {
|
||||
const token = crypto.randomBytes(32).toString("hex");
|
||||
// 3. Create Redis key
|
||||
const redisKey = `password-reset:${tokenHash}`;
|
||||
|
||||
const tokenHash = crypto
|
||||
.createHash("sha256")
|
||||
.update(token)
|
||||
.digest("hex");
|
||||
// 4. Save user ID with 15 minute TTL
|
||||
await redis.set(redisKey, userId, "EX", PASSWORD_RESET_TTL);
|
||||
|
||||
await redis.set(
|
||||
`passwordReset:user:${userId}`,
|
||||
tokenHash,
|
||||
"EX",
|
||||
RESET_TOKEN_TTL
|
||||
);
|
||||
log(`Generated password reset token for user ${userId} with TTL of ${RESET_TOKEN_TTL} seconds, Generated token:`, token);
|
||||
return token;
|
||||
} catch (error) {
|
||||
log("Password reset token generation failed", error);
|
||||
throw new Error("Failed to generate password reset token");
|
||||
// Send only RAW token to user
|
||||
return token;
|
||||
};
|
||||
|
||||
const verifyPasswordResetToken = async (token) => {
|
||||
if (!token || typeof token !== "string") {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset password using 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}`
|
||||
);
|
||||
// Hash token received from user
|
||||
const tokenHash = hashPasswordResetToken(token);
|
||||
|
||||
if (!storedHash) {
|
||||
throw new Error("Invalid or expired reset token");
|
||||
}
|
||||
// Create same Redis key
|
||||
const redisKey = `password-reset:${tokenHash}`;
|
||||
|
||||
const tokenHash = crypto
|
||||
.createHash("sha256")
|
||||
.update(token)
|
||||
.digest("hex");
|
||||
// Search Redis
|
||||
const userId = await redis.get(redisKey);
|
||||
|
||||
const isValid =
|
||||
crypto.timingSafeEqual(
|
||||
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;
|
||||
if (!userId) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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 = {
|
||||
generateResetToken,
|
||||
resetPassword
|
||||
createPasswordReset,
|
||||
verifyPasswordResetToken,
|
||||
deletePasswordReset,
|
||||
hashPasswordResetToken,
|
||||
sendPasswordResetEmail,
|
||||
sendPasswordChangedEmail,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user