/**
* @fileoverview Service for managing user operations in Satoshi Showdown.
* This service provides comprehensive functionalities for user management, including
* registration, authentication, profile retrieval, and updates. It caters to both
* registered and guest users, playing a critical role in user data management and ensuring
* secure and efficient operations within the application. The service interacts with the
* user data model and utilizes various utilities for validation, encryption, and logging.
*
* @module services/userService
* @requires models/userModel - User data model for database interactions.
* @requires bcrypt - Library for hashing passwords.
* @requires utils/validationUtil - Utility for validating user data.
* @requires utils/errorUtil - Custom error classes for consistent error handling.
* @requires utils/logUtil - Logging utility for application-wide logging.
*/
const User = require("../models/userModel");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const { validateUser } = require("../utils/validationUtil");
const { ValidationError, NotFoundError } = require("../utils/errorUtil");
const log = require("../utils/logUtil");
// Register User
const registerUser = async (userData) => {
// Validate user data
const { error } = validateUser(userData);
if (error) {
throw new ValidationError(error.details.map((d) => d.message).join("; "));
}
// Check if user already exists
if (
await User.findOne({
$or: [{ username: userData.username }, { email: userData.email }],
})
) {
throw new ValidationError("Username or email already exists");
}
// Hash password
const passwordHash = await bcrypt.hash(userData.password, 10);
// Create new user
const newUser = new User({
...userData,
passwordHash,
});
await newUser.save();
// Exclude sensitive data before returning
return excludeSensitiveData(newUser);
};
// Login User
const loginUser = async (username, password) => {
const user = await User.findOne({ username });
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
throw new Error("Invalid username or password");
}
const token = jwt.sign({ userId: user.userId }, process.env.JWT_SECRET, {
expiresIn: "1h",
});
return { token };
};
/**
* Retrieves a user's data by their unique ID.
* Essential for profile management, authentication, and accessing user-specific data within the application.
*
* @async
* @function getUserById
* @param {string} userId - The unique ID of the user to retrieve.
* @return {Promise<Object>} The user object with sensitive data excluded.
* @throws {NotFoundError} If no user is found with the provided ID.
*/
const getUserById = async (userId) => {
const user = await User.findOne({ userId });
if (!user) {
throw new NotFoundError(`User with ID ${userId} not found`);
}
return user;
};
/**
* Retrieves a user's data by their username.
* Used for operations like login, where the username is a key identifier.
*
* @async
* @function getUserByUsername
* @param {string} username - The username of the user to retrieve.
* @return {Promise<Object>} The user object with sensitive data excluded.
* @throws {NotFoundError} If no user is found with the provided username.
*/
const getUserByUsername = async (username) => {
const user = await User.findOne({ username });
if (!user) {
throw new NotFoundError(`User with username ${username} not found`);
}
return excludeSensitiveData(user);
};
/**
* Retrieves a user's data by their email.
* Critical for processes like password recovery or email-based authentication.
*
* @async
* @function getUserByEmail
* @param {string} email - The email of the user to retrieve.
* @return {Promise<Object>} The user object with sensitive data excluded.
* @throws {NotFoundError} If no user is found with the provided email.
*/
const getUserByEmail = async (email) => {
const user = await User.findOne({ email });
if (!user) {
throw new NotFoundError(`User with email ${email} not found`);
}
return excludeSensitiveData(user);
};
/**
* Retrieves a user's data by their IP address.
* Useful for tracking guest users or for security and auditing purposes.
*
* @async
* @function getUserByIP
* @param {string} ipAddress - The IP address of the user to retrieve.
* @return {Promise<Object>} The user object with sensitive data excluded.
* @throws {NotFoundError} If no user is found with the provided IP address.
*/
const getUserByIP = async (ipAddress) => {
const user = await User.findOne({ ipAddress });
if (!user) {
throw new NotFoundError(`User with IP address ${ipAddress} not found`);
}
return excludeSensitiveData(user);
};
/**
* Retrieves all users registered in the system.
* Typically used for administrative and reporting purposes, providing a comprehensive view of the user base.
*
* @async
* @function getAllUsers
* @return {Promise<Array<Object>>} An array of user objects with sensitive data excluded.
*/
const getAllUsers = async () => {
const users = await User.find({});
return users.map(excludeSensitiveData);
};
/**
* Updates the details of an existing user based on their ID.
* Allows modification of user information like contact details, preferences, and other profile data.
*
* @async
* @function updateUser
* @param {string} userId - The unique ID of the user to update.
* @param {Object} updateData - Data containing the updates for the user.
* @param {string} updateData.username - Updated username of the user.
* @param {string} updateData.email - Updated email address of the user.
* @param {string} updateData.password - Updated hashed password for security purposes.
* @param {Date} updateData.lastActive - Updated timestamp of the user's last activity.
* @param {string} updateData.role - Updated role of the user within the platform.
* @param {Object} updateData.profileInfo - Updated additional profile information (structure can vary).
* @param {string} updateData.ipAddress - Updated IP address of the user.
* @param {ObjectID} updateData.organization - Updated reference to an Organization, if applicable.
* @param {Array} updateData.eventsCreated - Updated events created by the user.
* @param {Array} updateData.eventsParticipated - Updated events in which the user has participated.
* @param {Array} updateData.transactions - Updated transactions associated with the user.
* @return {Promise<Object>} The updated user object with sensitive data (e.g., password hash) excluded.
* @throws {NotFoundError} If no user is found with the provided ID.
*/
const updateUser = async (userId, updateData) => {
const user = await User.findByIdAndUpdate(userId, updateData, { new: true });
if (!user) {
throw new NotFoundError(`User with ID ${userId} not found`);
}
return excludeSensitiveData(user);
};
/**
* Deletes a user from the system based on their unique ID.
* Essential for account management, data privacy compliance, and user-requested account deletion.
*
* @async
* @function deleteUser
* @param {string} userId - The unique ID of the user to delete.
* @return {Promise<void>} Indicates successful deletion of the user.
* @throws {NotFoundError} If no user is found with the provided ID.
*/
const deleteUser = async (userId) => {
const user = await User.findById(userId);
if (!user) {
throw new NotFoundError(`User with ID ${userId} not found`);
}
await user.remove();
log.info(`User with ID ${userId} deleted successfully`);
};
/**
* Checks if a username or email already exists in the database.
* Used internally to prevent duplicate user registrations.
*
* @private
* @async
* @function checkUserExists
* @param {string} username - The username to check for existence.
* @param {string} email - The email to check for existence.
* @return {Promise<boolean>} True if the username or email exists, false otherwise.
*/
async function checkUserExists(username, email) {
const userCount = await User.countDocuments({
$or: [{ username }, { email }],
});
return userCount > 0;
}
/**
* Excludes sensitive data from the user object before returning it.
* Ensures that information like password hashes is not exposed outside the service.
*
* @private
* @function excludeSensitiveData
* @param {User} user - The user object to sanitize.
* @return {Object} The user object without sensitive data.
*/
const excludeSensitiveData = (user) => {
const userObject = user.toObject();
delete userObject.passwordHash;
delete userObject.__v;
delete userObject.lastActive;
delete userObject.createdAt;
delete userObject.updatedAt;
delete userObject._id;
delete userObject.ipAddress;
return userObject;
};
module.exports = {
registerUser,
loginUser,
getUserById,
getUserByUsername,
getUserByEmail,
getUserByIP,
getAllUsers,
updateUser,
deleteUser,
};