636 lines
14 KiB
JavaScript
636 lines
14 KiB
JavaScript
/**
|
|
* Copyright (c) 2026 Niolla
|
|
* All rights reserved.
|
|
*
|
|
* This source code is proprietary and confidential.
|
|
* Unauthorized copying, modification, distribution, or use
|
|
* of this file, via any medium, is strictly prohibited.
|
|
*/
|
|
|
|
// 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) => {
|
|
const transaction = await db.sequelize.transaction();
|
|
|
|
try {
|
|
const {
|
|
firstName,
|
|
lastName,
|
|
email,
|
|
password,
|
|
} = 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();
|
|
|
|
return res.status(400).send({
|
|
success: false,
|
|
message: "User with this email already exists",
|
|
});
|
|
}
|
|
|
|
// let pass;
|
|
|
|
// if(accountType === "admin" || accountType === "superadmin" || accountType === "manager" || accountType === "support_agent") {
|
|
// pass = process.env.DEFAULT_PASSWORD;
|
|
// }else{
|
|
// pass = password;
|
|
// }
|
|
|
|
const validatePasswordResult = validatePassword(password);
|
|
|
|
if (!validatePasswordResult) {
|
|
await transaction.rollback();
|
|
return res.status(400).send({
|
|
success: false,
|
|
message: "Password does not meet the required criteria",
|
|
});
|
|
}
|
|
|
|
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(
|
|
{
|
|
id: userID,
|
|
firstName,
|
|
lastName,
|
|
email,
|
|
password: hashedPassword,
|
|
// accountType,
|
|
accountStatus: "PENDING_VERIFICATION",
|
|
emailVerifiedAt: null,
|
|
},
|
|
{ transaction },
|
|
);
|
|
|
|
|
|
const newProfile = await Profile.create(
|
|
{
|
|
profile_id: generateId(),
|
|
user_id: newUser.id,
|
|
theme: "light",
|
|
notificationsEnabled: true,
|
|
profilePicture_id: "N/A",
|
|
backgroundImage_id: "N/A",
|
|
dob: null,
|
|
phone_number: null,
|
|
},
|
|
{ transaction },
|
|
);
|
|
|
|
await EmailVerification.create(
|
|
{
|
|
id:
|
|
generateId(),
|
|
|
|
user_id:
|
|
newUser.id,
|
|
|
|
tokenHash:
|
|
verificationTokenHash,
|
|
|
|
expiresAt:
|
|
verificationExpiresAt,
|
|
|
|
usedAt:
|
|
null,
|
|
},
|
|
{
|
|
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}`,
|
|
type: "CREATE_USER",
|
|
module: "User Management",
|
|
});
|
|
|
|
res.status(201).send({
|
|
success: true,
|
|
message: "User Created Successfully",
|
|
data: {
|
|
id: newUser.id,
|
|
firstName: newUser.firstName,
|
|
lastName: newUser.lastName,
|
|
email: newUser.email,
|
|
accountType: newUser.accountType,
|
|
// role: newUser.role,
|
|
// department: newUser.department,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
await transaction.rollback();
|
|
|
|
res.status(500).send({
|
|
success: false,
|
|
message: "Failed to create user",
|
|
error: error.message,
|
|
});
|
|
}
|
|
};
|
|
|
|
// 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 {
|
|
const page = parseInt(req.query.page) || 1; // default page = 1
|
|
const limit = 20;
|
|
const offset = (page - 1) * limit;
|
|
|
|
const { count, rows: users } = await User.findAndCountAll({
|
|
attributes: { exclude: ["password"] },
|
|
limit,
|
|
offset,
|
|
order: [["createdAt", "DESC"]], // optional sorting
|
|
});
|
|
|
|
res.status(200).send({
|
|
success: true,
|
|
data: users,
|
|
pagination: {
|
|
totalUsers: count,
|
|
totalPages: Math.ceil(count / limit),
|
|
currentPage: page,
|
|
pageSize: limit,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
res.status(500).send({
|
|
success: false,
|
|
message: "Failed to retrieve users",
|
|
error: error.message,
|
|
});
|
|
}
|
|
};
|
|
|
|
// Get user details by ID
|
|
exports.getUserById = async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const user = await User.findOne({
|
|
where: { id },
|
|
attributes: { exclude: ["password"] },
|
|
include: [{ model: Profile, as: "profile" }],
|
|
});
|
|
|
|
if (!user) {
|
|
return res.status(404).send({
|
|
success: false,
|
|
message: "User not found",
|
|
});
|
|
}
|
|
|
|
res.status(200).send({
|
|
success: true,
|
|
data: user,
|
|
});
|
|
} catch (error) {
|
|
res.status(500).send({
|
|
success: false,
|
|
message: "Failed to retrieve user",
|
|
error: error.message,
|
|
});
|
|
}
|
|
};
|
|
|
|
// Update user details
|
|
exports.updateUser = async (req, res) => {
|
|
const transaction = await db.sequelize.transaction();
|
|
try {
|
|
const { id } = req.params;
|
|
const {
|
|
firstName,
|
|
lastName,
|
|
email,
|
|
role,
|
|
roleID,
|
|
accountType,
|
|
department,
|
|
theme,
|
|
notificationsEnabled,
|
|
profilePicture_id,
|
|
backgroundImage_id,
|
|
dob,
|
|
phone_number,
|
|
} = req.body;
|
|
const user = await User.findOne({
|
|
where: { id },
|
|
});
|
|
const UserProfile = await Profile.findOne({
|
|
where: { user_id: id },
|
|
});
|
|
|
|
if (!user) {
|
|
return res.status(404).send({
|
|
success: false,
|
|
message: "User not found",
|
|
});
|
|
}
|
|
|
|
if (!UserProfile) {
|
|
return res.status(404).send({
|
|
success: false,
|
|
message: "User profile not found",
|
|
});
|
|
}
|
|
|
|
user.firstName = firstName || user.firstName;
|
|
user.lastName = lastName || user.lastName;
|
|
user.role = role || user.role;
|
|
user.roleID = roleID || user.roleID || "N/A";
|
|
user.accountType = accountType || user.accountType;
|
|
user.department = department || user.department;
|
|
|
|
UserProfile.theme = theme || UserProfile.theme;
|
|
UserProfile.notificationsEnabled =
|
|
notificationsEnabled !== undefined
|
|
? notificationsEnabled
|
|
: UserProfile.notificationsEnabled;
|
|
UserProfile.profilePicture_id =
|
|
profilePicture_id || UserProfile.profilePicture_id || "N/A";
|
|
UserProfile.backgroundImage_id =
|
|
backgroundImage_id || UserProfile.backgroundImage_id || "N/A";
|
|
UserProfile.dob = dob || UserProfile.dob;
|
|
UserProfile.phone_number = phone_number || UserProfile.phone_number;
|
|
|
|
await UserProfile.save({ transaction });
|
|
await user.save({ transaction });
|
|
await transaction.commit();
|
|
|
|
await logActivity({
|
|
user: req.user,
|
|
description: `Updated User with ID: ${user.id}`,
|
|
type: "UPDATE_USER",
|
|
module: "User Management",
|
|
});
|
|
|
|
const data = {
|
|
id: user.id,
|
|
firstName: user.firstName,
|
|
lastName: user.lastName,
|
|
email: user.email,
|
|
accountType: user.accountType,
|
|
role: user.role,
|
|
department: user.department,
|
|
profile: {
|
|
theme: UserProfile.theme,
|
|
notificationsEnabled: UserProfile.notificationsEnabled,
|
|
profilePicture_id: UserProfile.profilePicture_id,
|
|
backgroundImage_id: UserProfile.backgroundImage_id,
|
|
dob: UserProfile.dob,
|
|
phone_number: UserProfile.phone_number,
|
|
},
|
|
};
|
|
|
|
res.status(200).send({
|
|
success: true,
|
|
message: "User updated successfully",
|
|
data,
|
|
});
|
|
} catch (error) {
|
|
await transaction.rollback();
|
|
res.status(500).send({
|
|
success: false,
|
|
message: `Error: ${error.message}`,
|
|
});
|
|
}
|
|
};
|
|
|
|
// Delete user
|
|
exports.deleteUser = async (req, res) => {
|
|
const transaction = await db.sequelize.transaction();
|
|
try {
|
|
const { id } = req.params;
|
|
const user = await User.findOne({
|
|
where: { id },
|
|
});
|
|
if (!user) {
|
|
return res.status(404).send({
|
|
success: false,
|
|
message: "User not found",
|
|
});
|
|
}
|
|
await user.destroy({ transaction });
|
|
await transaction.commit();
|
|
|
|
await logActivity({
|
|
user: req.user,
|
|
description: `Deleted User with ID: ${user.id}`,
|
|
type: "DELETE_USER",
|
|
module: "User Management",
|
|
});
|
|
|
|
res.status(200).send({
|
|
success: true,
|
|
message: "User deleted successfully",
|
|
});
|
|
} catch (error) {
|
|
await transaction.rollback();
|
|
res.status(500).send({
|
|
success: false,
|
|
message: `Error: ${error.message}`,
|
|
});
|
|
}
|
|
};
|