From 621d348eb18e163b9ca981ed54e349d52fbdb8b2 Mon Sep 17 00:00:00 2001 From: Isuru Bimsara Date: Thu, 20 Aug 2026 15:16:17 +0530 Subject: [PATCH] add the redis --- app/config/mail.config.js | 2 +- app/controllers/user.controller.js | 354 +++++++++------------ app/models/index.js | 1 - app/models/user/emailVerification.model.js | 46 --- app/models/user/user.model.js | 4 - app/utils/emailVerification.util.js | 99 +++++- 6 files changed, 244 insertions(+), 262 deletions(-) delete mode 100644 app/models/user/emailVerification.model.js diff --git a/app/config/mail.config.js b/app/config/mail.config.js index d96e447..7096483 100644 --- a/app/config/mail.config.js +++ b/app/config/mail.config.js @@ -20,5 +20,5 @@ module.exports = { user: process.env.MAIL_USER, pass: process.env.MAIL_PASS, }, - from: `Oceanic Maritime Solutions <${process.env.MAIL_FROM}>`, + from: `ZUMRI <${process.env.MAIL_FROM}>`, }; diff --git a/app/controllers/user.controller.js b/app/controllers/user.controller.js index 2f5f845..caeeda1 100644 --- a/app/controllers/user.controller.js +++ b/app/controllers/user.controller.js @@ -9,7 +9,7 @@ // app/controllers/user.controller.js -const { Op } = require("sequelize"); +// const { Op } = require("sequelize"); const db = require("../models"); const { hashPassword } = require("../utils/hashPassword.util"); const { validatePassword } = require("../utils/validation/validatePassword.util"); @@ -17,10 +17,11 @@ const { validateEmail } = require("../utils/validation/validateEmail.util"); const { generateUserId, generateId } = require("../utils/idGen.util"); const { logActivity } = require("../services/activity.service"); const { sendMail } = require("../utils/mail.util"); -const {generateEmailVerificationToken, hashEmailVerificationToken} = require("../utils/emailVerification.util"); +const { + createEmailVerification, verifyEmailVerificationToken, deleteEmailVerification} = require("../utils/emailVerification.util");; const User = db.User; const Profile = db.Profile; -const EmailVerification = db.EmailVerification; +// const EmailVerification = db.EmailVerification; // Create a new user exports.createNewUser = async (req, res) => { @@ -84,20 +85,8 @@ if (!emailValid) { } const hashedPassword = await hashPassword(password); + const userID = generateUserId(); - const verificationToken = generateEmailVerificationToken(); - - const verificationTokenHash = - hashEmailVerificationToken( - verificationToken - ); - - const verificationExpiresAt = - new Date( - Date.now() + - 30 * 60 * 1000 - ); - const newUser = await User.create( { @@ -128,37 +117,13 @@ if (!emailValid) { { transaction }, ); - await EmailVerification.create( - { - id: - generateId(), - - user_id: - newUser.id, - - tokenHash: - verificationTokenHash, - - expiresAt: - verificationExpiresAt, - - usedAt: - null, - }, - { - transaction, - } - ); - await transaction.commit(); + const verificationToken = await createEmailVerification(newUser.id); + const confirmationLink = `${process.env.FRONTEND_URL}/verify-email?token=${verificationToken}`; - - // ================================================== - // 18. Send verification email - // ================================================== try { @@ -188,15 +153,6 @@ if (!emailValid) { } catch (mailError) { - // The database transaction is already committed. - // - // Do NOT delete the user here. - // - // User remains: - // PENDING_VERIFICATION - // - // Later resend-verification can send another email. - console.error( "VERIFICATION EMAIL ERROR:", mailError @@ -246,13 +202,20 @@ if (!emailValid) { firstName: newUser.firstName, lastName: newUser.lastName, email: newUser.email, - accountType: newUser.accountType, + // accountType: newUser.accountType, // role: newUser.role, // department: newUser.department, }, }); } catch (error) { - await transaction.rollback(); + if (!transaction.finished) { + await transaction.rollback(); + } + + console.error( + "CREATE USER ERROR:", + error + ); res.status(500).send({ success: false, @@ -263,173 +226,168 @@ if (!emailValid) { }; // Verify customer email -exports.verifyEmail = async (req, res) => { +exports.verifyEmail = + async (req, res) => { - const transaction = - await db.sequelize.transaction(); - - try { - - // 1. Get token from request body - const { - token - } = req.body; + const transaction = + await db.sequelize.transaction(); - // 2. Token is required - if (!token) { + try { + const { + token, + } = req.body; - await transaction.rollback(); - return res.status(400).send({ - success: false, - message: - "Verification token is required", + if (!token) { + + await transaction.rollback(); + + + return res.status(400).send({ + success: + false, + + message: + "Verification token is required", + }); + } + + const verification = + await verifyEmailVerificationToken( + token + ); + + if (!verification) { + + await transaction.rollback(); + + + return res.status(400).send({ + success: + false, + + message: + "Verification token is invalid or expired", + }); + } + + + + const { + userId, + redisKey, + } = + verification; + + const user = + await User.findOne({ + where: { + id: + userId, + }, + + transaction, + }); + + + + if (!user) { + + await transaction.rollback(); + + await deleteEmailVerification( + redisKey + ); + + + return res.status(404).send({ + success: + false, + + message: + "User not found", + }); + } + + if ( + user.accountStatus === + "ACTIVE" && + user.emailVerifiedAt + ) { + + await transaction.rollback(); + + + // Token is no longer needed + await deleteEmailVerification( + redisKey + ); + + + return res.status(400).send({ + success: + false, + + message: + "Email is already verified", + }); + } + + + user.accountStatus = + "ACTIVE"; + + + user.emailVerifiedAt = + new Date(); + + + await user.save({ + transaction, }); - } + await transaction.commit(); - // 3. Hash the received raw token - const tokenHash = - hashEmailVerificationToken( - token + await deleteEmailVerification( + redisKey ); - // 4. Find matching valid token - const verification = - await EmailVerification.findOne({ - where: { + return res.status(200).send({ + success: + true, - tokenHash: - tokenHash, - - usedAt: - null, - - expiresAt: { - [Op.gt]: - new Date(), - }, - }, - - transaction, - }); - - - // 5. Token invalid / expired / already used - if (!verification) { - - await transaction.rollback(); - - return res.status(400).send({ - success: false, message: - "Verification token is invalid or expired", - }); - } - - - // 6. Find user - const user = - await User.findOne({ - - where: { - id: - verification.user_id, - }, - - transaction, + "Email verified successfully. Your account is now active.", }); - // 7. User not found - if (!user) { + } catch (error) { - await transaction.rollback(); + if (!transaction.finished) { + + await transaction.rollback(); + + } + + + console.error( + "VERIFY EMAIL ERROR:", + error + ); + + + return res.status(500).send({ + success: + false, - return res.status(404).send({ - success: false, message: - "User not found", + "Failed to verify email", }); } - - - // 8. Check already verified - if ( - user.accountStatus === "ACTIVE" && - user.emailVerifiedAt - ) { - - await transaction.rollback(); - - return res.status(400).send({ - success: false, - message: - "Email is already verified", - }); - } - - - // 9. Activate account - user.accountStatus = - "ACTIVE"; - - user.emailVerifiedAt = - new Date(); - - - await user.save({ - transaction, - }); - - - // 10. Mark token as used - verification.usedAt = - new Date(); - - - await verification.save({ - transaction, - }); - - - // 11. Commit - await transaction.commit(); - - - // 12. Success - return res.status(200).send({ - success: true, - message: - "Email verified successfully. Your account is now active.", - }); - - - } catch (error) { - - if (!transaction.finished) { - - await transaction.rollback(); - - } - - - console.error( - "VERIFY EMAIL ERROR:", - error - ); - - - return res.status(500).send({ - success: false, - message: - "Failed to verify email", - }); - } -}; + }; // Get users with pagination (20 per page) exports.getAllUsers = async (req, res) => { diff --git a/app/models/index.js b/app/models/index.js index eed698c..d695284 100644 --- a/app/models/index.js +++ b/app/models/index.js @@ -41,7 +41,6 @@ db.sequelize = sequelize; // User and Authentication db.User = require("./user/user.model")(sequelize, DataTypes); -db.EmailVerification = require("./user/emailVerification.model")(sequelize,DataTypes); db.UserActivity = require("./activities/userActivities.model")(sequelize, DataTypes); db.Profile = require("./user/profile.model")(sequelize, DataTypes); diff --git a/app/models/user/emailVerification.model.js b/app/models/user/emailVerification.model.js deleted file mode 100644 index a6e8517..0000000 --- a/app/models/user/emailVerification.model.js +++ /dev/null @@ -1,46 +0,0 @@ -//app/models/user/emailVerification.model.js -module.exports = (sequelize, DataTypes) => { - const EmailVerification = sequelize.define( - "EmailVerification", - { - id: { - type: DataTypes.STRING, - primaryKey: true, - }, - - user_id: { - type: DataTypes.STRING, - allowNull: false, - }, - - tokenHash: { - type: DataTypes.STRING, - allowNull: false, - }, - - expiresAt: { - type: DataTypes.DATE, - allowNull: false, - }, - - usedAt: { - type: DataTypes.DATE, - allowNull: true, - defaultValue: null, - }, - }, - { - tableName: "email_verifications", - timestamps: true, - } - ); - - EmailVerification.associate = (db) => { - EmailVerification.belongsTo(db.User, { - foreignKey: "user_id", - as: "user", - }); - }; - - return EmailVerification; -}; \ No newline at end of file diff --git a/app/models/user/user.model.js b/app/models/user/user.model.js index 4765ff6..e73e011 100644 --- a/app/models/user/user.model.js +++ b/app/models/user/user.model.js @@ -72,10 +72,6 @@ module.exports = (sequelize, DataTypes) => { as: "profile", }); - User.hasMany(db.EmailVerification, { - foreignKey: "user_id", - as: "emailVerifications", - }); }; return User; diff --git a/app/utils/emailVerification.util.js b/app/utils/emailVerification.util.js index 026e00d..2ab997d 100644 --- a/app/utils/emailVerification.util.js +++ b/app/utils/emailVerification.util.js @@ -1,19 +1,20 @@ -//app/utils/emailVerification.util.js +// app/utils/emailVerification.util.js + const crypto = require("crypto"); -/** - * Generate a cryptographically secure - * random email-verification token. - */ +const redis = require("../config/redisClient"); + +const EMAIL_VERIFICATION_TTL = + Number( + process.env.EMAIL_VERIFICATION_TTL_SECONDS + ) || 1800; + const generateEmailVerificationToken = () => { - return crypto.randomBytes(32).toString("hex"); + return crypto + .randomBytes(32) + .toString("hex"); }; - -/** - * Hash verification token before - * storing it in database. - */ const hashEmailVerificationToken = (token) => { return crypto .createHash("sha256") @@ -21,8 +22,82 @@ const hashEmailVerificationToken = (token) => { .digest("hex"); }; +const createEmailVerification = async (userId) => { + + // 1. Generate raw token + const token = + generateEmailVerificationToken(); + + + // 2. Hash token + const tokenHash = + hashEmailVerificationToken(token); + + + // 3. Create Redis key + const redisKey = + `email-verification:${tokenHash}`; + + await redis.set( + redisKey, + userId, + "EX", + EMAIL_VERIFICATION_TTL + ); + + return token; +}; + + +/** + * Check a verification token. + */ +const verifyEmailVerificationToken = + async (token) => { + + if ( + !token || + typeof token !== "string" + ) { + return null; + } + + + // 1. Hash token received from user + const tokenHash = + hashEmailVerificationToken(token); + + + // 2. Build same Redis key + const redisKey = + `email-verification:${tokenHash}`; + + + // 3. Search Redis + const userId = + await redis.get(redisKey); + + if (!userId) { + return null; + } + + + return { + userId, + redisKey, + }; + }; + +const deleteEmailVerification = + async (redisKey) => { + + await redis.del(redisKey); + }; + module.exports = { - generateEmailVerificationToken, + createEmailVerification, + verifyEmailVerificationToken, + deleteEmailVerification, hashEmailVerificationToken, }; \ No newline at end of file