Files
Zumri-Backend/app/controllers/user.controller.js
Sathira Sri Sathara 9d3d431416 feat: implement identity and security features
- Added account types and privileged account types constants.
- Created admin user controller for updating user security fields.
- Developed role assignment controller for managing user roles.
- Implemented validation middleware for request schemas.
- Defined user role and auth session models for database interactions.
- Created services for authentication, email notifications, and OTP handling.
- Developed OAuth service for Google and Apple authentication.
- Added JWT utility functions for token generation and verification.
- Implemented comprehensive tests for authentication, session management, and password policies.
- Created migration for updating user schema and adding new tables for auth sessions and user identities.
2026-09-03 13:56:18 +05:30

567 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 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 {
createCustomerDetails,
} = require("../utils/users/createCustomerDetails.util");
const {
createBusinessCustomerDetails,
} = require("../utils/users/createBusinessCustomer.util");
const { logActivity } = require("../services/activity.service");
const { sendMail } = require("../utils/mail.util");
const {
createEmailVerification,
verifyEmailVerificationToken,
sendVerificationEmail,
deleteEmailVerification,
} = require("../utils/emailVerification.util");
const { getUserProfile } = require("../utils/users/userProfileDetails.util");
const User = db.User;
const Profile = db.Profile;
// Create a new user
exports.createNewUser = async (req, res) => {
const transaction = await db.sequelize.transaction();
try {
const {
firstName,
lastName,
email,
password,
address,
phoneNumber,
businessName,
businessRegistrationNumber,
businessType,
contactName,
businessEmail,
expectedMonthlyVolume,
note,
} = 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",
});
}
if (req.body.accountType && req.body.accountType !== "customer") {
await transaction.rollback();
return res.status(400).send({
success: false,
message:
"Public registration creates customer accounts only",
});
}
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",
});
}
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 newUser = await User.create(
{
id: userID,
firstName,
lastName,
email,
password: hashedPassword,
accountType: "customer",
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 },
);
// Create the customer identity extension for public registration.
{
const customerData = {
address,phoneNumber,
};
await createCustomerDetails(
newUser.id,
customerData,
transaction,
);
}
await transaction.commit();
const verificationToken = await createEmailVerification(newUser.id);
try {
await sendVerificationEmail(
newUser.email,
newUser.firstName,
verificationToken,
);
} catch (error) {
console.error("Error sending verification email:", error);
}
await logActivity({
user: newUser,
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,
},
});
} catch (error) {
if (!transaction.finished) {
await transaction.rollback();
}
console.error("CREATE USER ERROR:", error);
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 {
const { token } = req.body;
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();
await deleteEmailVerification(redisKey);
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 profile details
exports.userProfile = async (req, res) => {
try {
const userId = req.user.id;
const profile =
await getUserProfile(userId);
if (!profile) {
return res.status(404).send({
success: false,
message: "User not found",
});
}
return res.status(200).send({
success: true,
message:
"User profile retrieved successfully",
data: profile,
});
} catch (error) {
console.error(
"GET USER PROFILE ERROR:",
error
);
return res.status(500).send({
success: false,
message:
"Failed to retrieve user profile",
});
}
};
// 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) {
await transaction.rollback();
return res.status(404).send({
success: false,
message: "User not found",
});
}
if (!UserProfile) {
await transaction.rollback();
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) {
await transaction.rollback();
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}`,
});
}
};
exports.getCurrentUser = async (req, res, next) => {
try {
const user = await User.findByPk(req.user.id, { attributes: { exclude: ["password", "tokenVersion", "passwordChangedAt"] }, include: [{ model: Profile, as: "profile" }] });
res.json({ success: true, data: user });
} catch (error) { next(error); }
};
exports.updateCurrentUser = async (req, res, next) => {
try {
const allowed = ["firstName", "lastName"];
const supplied = Object.keys(req.body);
if (supplied.some((key) => !allowed.includes(key))) return res.status(400).json({ success: false, error: { code: "UNSAFE_FIELD", message: "Only firstName and lastName may be updated" } });
const updates = Object.fromEntries(supplied.map((key) => [key, req.body[key]]).filter(([, value]) => typeof value === "string" && value.trim()));
await User.update(updates, { where: { id: req.user.id } });
const user = await User.findByPk(req.user.id, { attributes: ["id", "firstName", "lastName", "email", "accountType", "accountStatus"] });
res.json({ success: true, data: user });
} catch (error) { next(error); }
};