Implement user registration and email verification features

This commit is contained in:
Isuru Bimsara
2026-08-17 16:52:44 +05:30
parent ef8db0988b
commit c3d9f899a1
11 changed files with 781 additions and 53 deletions
+318 -23
View File
@@ -9,14 +9,18 @@
// 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 { 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 User = db.User;
const Profile = db.Profile;
const EmailVerification = db.EmailVerification;
// Create a new user
exports.createNewUser = async (req, res) => {
@@ -28,9 +32,29 @@ exports.createNewUser = async (req, res) => {
lastName,
email,
password,
accountType,
} = req.body;
if (!firstName || !lastName || !email || !password) {
await transaction.rollback();
return res.status(400).send({
success: false,
message:
"First name, last name, email and password are required",
});
}
const emailValid = validateEmail(email);
if (!emailValid) {
await transaction.rollback();
return res.status(400).send({
success: false,
message: "Invalid email address",
});
}
const userExists = await User.findOne({ where: { email } });
if (userExists) {
await transaction.rollback();
@@ -41,15 +65,15 @@ exports.createNewUser = async (req, res) => {
});
}
let pass;
// let pass;
if(accountType === "admin" || accountType === "superadmin" || accountType === "manager" || accountType === "support_agent") {
pass = process.env.DEFAULT_PASSWORD;
}else{
pass = password;
}
// if(accountType === "admin" || accountType === "superadmin" || accountType === "manager" || accountType === "support_agent") {
// pass = process.env.DEFAULT_PASSWORD;
// }else{
// pass = password;
// }
const validatePasswordResult = validatePassword(pass);
const validatePasswordResult = validatePassword(password);
if (!validatePasswordResult) {
await transaction.rollback();
@@ -59,10 +83,21 @@ exports.createNewUser = async (req, res) => {
});
}
const hashedPassword = await hashPassword(pass);
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(
{
@@ -71,7 +106,9 @@ exports.createNewUser = async (req, res) => {
lastName,
email,
password: hashedPassword,
accountType,
// accountType,
accountStatus: "PENDING_VERIFICATION",
emailVerifiedAt: null,
},
{ transaction },
);
@@ -91,20 +128,109 @@ exports.createNewUser = async (req, res) => {
{ transaction },
);
await sendMail({
to: email,
subject: "Welcome - Ocenic Titan",
templateName: "welcome",
templateVars: {
firstName: firstName,
email: email,
password: process.env.DEFAULT_PASSWORD,
await EmailVerification.create(
{
id:
generateId(),
user_id:
newUser.id,
tokenHash:
verificationTokenHash,
expiresAt:
verificationExpiresAt,
usedAt:
null,
},
text: `Hello ${firstName}, your account has been created successfully.`,
});
{
transaction,
}
);
await transaction.commit();
const confirmationLink =
`${process.env.FRONTEND_URL}/verify-email?token=${verificationToken}`;
// ==================================================
// 18. Send verification email
// ==================================================
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) {
// 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
);
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:
newUser.email,
accountType:
newUser.accountType,
accountStatus:
newUser.accountStatus,
},
});
}
await logActivity({
user: req.user,
description: `Created New User with ID: ${newUser.id}`,
@@ -121,8 +247,8 @@ exports.createNewUser = async (req, res) => {
lastName: newUser.lastName,
email: newUser.email,
accountType: newUser.accountType,
role: newUser.role,
department: newUser.department,
// role: newUser.role,
// department: newUser.department,
},
});
} catch (error) {
@@ -136,6 +262,175 @@ exports.createNewUser = async (req, res) => {
}
};
// Verify customer email
exports.verifyEmail = async (req, res) => {
const transaction =
await db.sequelize.transaction();
try {
// 1. Get token from request body
const {
token
} = req.body;
// 2. Token is required
if (!token) {
await transaction.rollback();
return res.status(400).send({
success: false,
message:
"Verification token is required",
});
}
// 3. Hash the received raw token
const tokenHash =
hashEmailVerificationToken(
token
);
// 4. Find matching valid token
const verification =
await EmailVerification.findOne({
where: {
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,
});
// 7. User not found
if (!user) {
await transaction.rollback();
return res.status(404).send({
success: false,
message:
"User not found",
});
}
// 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) => {
try {