import { logger } from "../config/logger.js";
import { User } from "../models/User.model.js";
import { PasswordUtils } from "../utils/password.utils.js";
/**
* Service responsible for privileged user management operations.
* @category Services
*/
export class AdminService {
/**
* Create a new user using the provided payload.
* @param {CreateUserRequest} userData User creation payload from gRPC contract.
* @returns {Promise<TUser>} Newly created gRPC user representation.
*/
async createUser(userData) {
try {
const { email, name, password, role } = userData;
// Check if user already exists
const existingUser = await User.findOne({ email: email.toLowerCase() });
if (existingUser) {
throw new Error("User with this email already exists");
}
// Hash password
const passwordHash = await PasswordUtils.hashPassword(password);
// Create user
const user = new User({
email: email.toLowerCase(),
name,
passwordHash,
role,
});
await user.save();
logger.info(`User created: ${email} with role: ${role}`);
return {
id: user._id.toString(),
email: user.email,
name: user.name,
role,
};
}
catch (error) {
logger.error({
error,
message: "User creation failed",
});
throw error;
}
}
/**
* Reset a user's password by email.
* @param {IResetPasswordDto} resetData DTO containing the email and new password.
* @returns {Promise<void>} Resolves when the password is updated.
*/
async resetPassword(resetData) {
try {
const { email, newPassword } = resetData;
const user = await User.findOne({ email: email.toLowerCase() });
if (!user) {
throw new Error("User not found");
}
// Hash new password
const passwordHash = await PasswordUtils.hashPassword(newPassword);
// Update password
user.passwordHash = passwordHash;
await user.save();
logger.info(`Password reset for user: ${email}`);
}
catch (error) {
logger.error({
error,
message: "Password reset failed",
});
throw error;
}
}
/**
* Delete a user ensuring admins cannot remove themselves.
* @param {string} email Target user's email.
* @param {string} currentUserEmail Authenticated admin's email.
* @returns {Promise<void>} Resolves when deletion completes.
*/
async deleteUser(email, currentUserEmail) {
try {
logger.info({ email, currentUserEmail, message: "Deleting user by email" });
// Prevent admin from deleting themselves
if (email.toLowerCase() === currentUserEmail.toLowerCase()) {
throw new Error("Admins cannot delete their own account");
}
const user = await User.findOne({ email: email.toLowerCase() });
if (!user) {
throw new Error("User not found");
}
// Delete avatar from GCS if exists
if (user.avatar) {
try {
const { gcsService } = await import("./gcs.service.js");
await gcsService.deleteAvatar(user.avatar);
}
catch (error) {
logger.warn({ error }, "Failed to delete avatar from GCS:");
}
}
// Delete user
await User.findByIdAndDelete(user._id);
logger.info(`User deleted: ${email}`);
}
catch (error) {
logger.error({
error,
message: "User deletion by email failed",
});
throw error;
}
}
/**
* Retrieve paginated SMEs/admins excluding the current user.
* @param {string} currentUserId Authenticated admin ID to exclude.
* @param {number} page Page number (default: 1).
* @param {number} limit Page size (default: 10).
* @param {string} [search] Optional search term.
* @returns {Promise<IPaginatedResponse<ISmeResponse>>} Paginated response payload.
*/
async getAllSmes(currentUserId, page = 1, limit = 10, search) {
try {
const query = {
_id: { $ne: currentUserId },
};
if (search) {
query.$or = [
{ email: { $regex: search, $options: "i" } },
{ name: { $regex: search, $options: "i" } },
];
}
const skip = (page - 1) * limit;
const total = await User.countDocuments(query);
const users = await User.find(query)
.select("-passwordHash")
.sort({ createdAt: -1 }) // Sort by newest first
.skip(skip)
.limit(limit)
.lean();
const data = users.map((user) => ({
id: user._id.toString(),
email: user.email,
name: user.name,
role: user.role,
avatar: user.avatar || null,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
}));
logger.info({
page,
limit,
total,
search,
message: "Retrieved admins and SMEs with pagination",
});
return {
data,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
};
}
catch (error) {
logger.error({
error,
message: "Failed to retrieve admins and SMEs",
});
throw error;
}
}
}
Source