Implement password reset and change features with email notifications

This commit is contained in:
Isuru Bimsara
2026-08-21 09:56:54 +05:30
parent b2c5e198a1
commit 6a4fe852e5
6 changed files with 597 additions and 138 deletions
+107 -84
View File
@@ -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,
};