refactor module into class

This commit is contained in:
Alex Holliday
2025-07-30 10:41:53 -07:00
parent e7cf6d385c
commit 8adad64248
4 changed files with 174 additions and 263 deletions

View File

@@ -34,7 +34,10 @@ import crypto from "crypto";
// DB Modules
import { NormalizeData } from "../utils/dataUtils.js";
import { GenerateAvatarImage } from "../utils/imageProcessing.js";
import { ParseBoolean } from "../utils/utils.js";
// Models
import Check from "../db/models/Check.js";
import HardwareCheck from "../db/models/HardwareCheck.js";
import PageSpeedCheck from "../db/models/PageSpeedCheck.js";
@@ -42,10 +45,12 @@ import Monitor from "../db/models/Monitor.js";
import User from "../db/models/User.js";
import InviteToken from "../db/models/InviteToken.js";
import StatusPage from "../db/models/StatusPage.js";
import Team from "../db/models/Team.js";
import InviteModule from "../db/mongo/modules/inviteModule.js";
import CheckModule from "../db/mongo/modules/checkModule.js";
import StatusPageModule from "../db/mongo/modules/statusPageModule.js";
import UserModule from "../db/mongo/modules/userModule.js";
export const initializeServices = async ({ logger, envSettings, settingsService }) => {
const serviceRegistry = new ServiceRegistry({ logger });
@@ -60,7 +65,9 @@ export const initializeServices = async ({ logger, envSettings, settingsService
const checkModule = new CheckModule({ logger, Check, HardwareCheck, PageSpeedCheck, Monitor, User });
const inviteModule = new InviteModule({ InviteToken, crypto, stringService });
const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService });
const db = new MongoDB({ logger, envSettings, checkModule, inviteModule, statusPageModule });
const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService });
const db = new MongoDB({ logger, envSettings, checkModule, inviteModule, statusPageModule, userModule });
await db.connect();
const networkService = new NetworkService(axios, ping, logger, http, Docker, net, stringService, settingsService);

View File

@@ -1,12 +1,6 @@
import mongoose from "mongoose";
import AppSettings from "../models/AppSettings.js";
//****************************************
// User Operations
//****************************************
import * as userModule from "./modules/userModule.js";
//****************************************
// Recovery Operations
//****************************************
@@ -52,10 +46,10 @@ import * as diagnosticModule from "./modules/diagnosticModule.js";
class MongoDB {
static SERVICE_NAME = "MongoDB";
constructor({ logger, envSettings, checkModule, inviteModule, statusPageModule }) {
constructor({ logger, envSettings, checkModule, inviteModule, statusPageModule, userModule }) {
this.logger = logger;
this.envSettings = envSettings;
Object.assign(this, userModule);
this.userModule = userModule;
this.inviteModule = inviteModule;
Object.assign(this, recoveryModule);
Object.assign(this, monitorModule);

View File

@@ -1,261 +1,171 @@
import UserModel from "../../models/User.js";
import TeamModel from "../../models/Team.js";
import { GenerateAvatarImage } from "../../../utils/imageProcessing.js";
const DUPLICATE_KEY_CODE = 11000; // MongoDB error code for duplicate key
import { ParseBoolean } from "../../../utils/utils.js";
import ServiceRegistry from "../../../service/system/serviceRegistry.js";
import StringService from "../../../service/system/stringService.js";
const SERVICE_NAME = "userModule";
const DUPLICATE_KEY_CODE = 11000; // MongoDB error code for duplicate key
const checkSuperadmin = async () => {
const superAdmin = await UserModel.findOne({ role: "superadmin" });
if (superAdmin !== null) {
return true;
class UserModule {
constructor({ User, Team, GenerateAvatarImage, ParseBoolean, stringService }) {
this.User = User;
this.Team = Team;
this.GenerateAvatarImage = GenerateAvatarImage;
this.ParseBoolean = ParseBoolean;
this.stringService = stringService;
}
return false;
};
/**
* Insert a User
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<UserModel>}
* @throws {Error}
*/
const insertUser = async (userData, imageFile, generateAvatarImage = GenerateAvatarImage) => {
const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
try {
if (imageFile) {
// 1. Save the full size image
userData.profileImage = {
data: imageFile.buffer,
contentType: imageFile.mimetype,
};
checkSuperadmin = async () => {
try {
const superAdmin = await this.User.findOne({ role: "superadmin" });
if (superAdmin !== null) {
return true;
}
return false;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "checkSuperadmin";
throw error;
}
};
// 2. Get the avatar sized image
const avatar = await generateAvatarImage(imageFile);
userData.avatarImage = avatar;
insertUser = async (userData, imageFile) => {
try {
if (imageFile) {
// 1. Save the full size image
userData.profileImage = {
data: imageFile.buffer,
contentType: imageFile.mimetype,
};
// 2. Get the avatar sized image
const avatar = await this.GenerateAvatarImage(imageFile);
userData.avatarImage = avatar;
}
// Handle creating team if superadmin
if (userData.role.includes("superadmin")) {
const team = new this.Team({
email: userData.email,
});
userData.teamId = team._id;
userData.checkTTL = 60 * 60 * 24 * 30;
await team.save();
}
const newUser = new this.User(userData);
await newUser.save();
return await this.User.findOne({ _id: newUser._id }).select("-password").select("-profileImage"); // .select() doesn't work with create, need to save then find
} catch (error) {
if (error.code === DUPLICATE_KEY_CODE) {
error.message = this.stringService.dbUserExists;
}
error.service = SERVICE_NAME;
error.method = "insertUser";
throw error;
}
};
getUserByEmail = async (email) => {
try {
// Need the password to be able to compare, removed .select()
// We can strip the hash before returning the user
const user = await this.User.findOne({ email: email }).select("-profileImage");
if (!user) {
throw new Error(this.stringService.dbUserNotFound);
}
return user;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getUserByEmail";
throw error;
}
};
updateUser = async ({ userId, user, file }) => {
if (!userId) {
throw new Error("No user in request");
}
// Handle creating team if superadmin
if (userData.role.includes("superadmin")) {
const team = new TeamModel({
email: userData.email,
});
userData.teamId = team._id;
userData.checkTTL = 60 * 60 * 24 * 30;
await team.save();
try {
const candidateUser = { ...user };
if (this.ParseBoolean(candidateUser.deleteProfileImage) === true) {
candidateUser.profileImage = null;
candidateUser.avatarImage = null;
} else if (file) {
// 1. Save the full size image
candidateUser.profileImage = {
data: file.buffer,
contentType: file.mimetype,
};
// 2. Get the avatar sized image
const avatar = await this.GenerateAvatarImage(file);
candidateUser.avatarImage = avatar;
}
const updatedUser = await this.User.findByIdAndUpdate(
userId,
candidateUser,
{ new: true } // Returns updated user instead of pre-update user
)
.select("-password")
.select("-profileImage");
return updatedUser;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "updateUser";
throw error;
}
const newUser = new UserModel(userData);
await newUser.save();
return await UserModel.findOne({ _id: newUser._id }).select("-password").select("-profileImage"); // .select() doesn't work with create, need to save then find
} catch (error) {
if (error.code === DUPLICATE_KEY_CODE) {
error.message = stringService.dbUserExists;
};
deleteUser = async (userId) => {
try {
const deletedUser = await this.User.findByIdAndDelete(userId);
if (!deletedUser) {
throw new Error(this.stringService.dbUserNotFound);
}
return deletedUser;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteUser";
throw error;
}
error.service = SERVICE_NAME;
error.method = "insertUser";
throw error;
}
};
};
/**
* Get User by Email
* Gets a user by Email. Not sure if we'll ever need this except for login.
* If not needed except for login, we can move password comparison here
* Throws error if user not found
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<UserModel>}
* @throws {Error}
*/
const getUserByEmail = async (email) => {
const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
try {
// Need the password to be able to compare, removed .select()
// We can strip the hash before returning the user
const user = await UserModel.findOne({ email: email }).select("-profileImage");
if (!user) {
throw new Error(stringService.dbUserNotFound);
getAllUsers = async () => {
try {
const users = await this.User.find().select("-password").select("-profileImage");
return users;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getAllUsers";
throw error;
}
return user;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getUserByEmail";
throw error;
}
};
};
/**
* Update a user by ID
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<UserModel>}
* @throws {Error}
*/
getUserById = async (roles, userId) => {
try {
if (!roles.includes("superadmin")) {
throw new Error("User is not a superadmin");
}
const updateUser = async ({ userId, user, file }) => {
if (!userId) {
throw new Error("No user in request");
}
const user = await this.User.findById(userId).select("-password").select("-profileImage");
if (!user) {
throw new Error("User not found");
}
try {
const candidateUser = { ...user };
if (ParseBoolean(candidateUser.deleteProfileImage) === true) {
candidateUser.profileImage = null;
candidateUser.avatarImage = null;
} else if (file) {
// 1. Save the full size image
candidateUser.profileImage = {
data: file.buffer,
contentType: file.mimetype,
};
// 2. Get the avatar sized image
const avatar = await GenerateAvatarImage(file);
candidateUser.avatarImage = avatar;
return user;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getUserById";
throw error;
}
};
// ******************************************
// End handling profile image
// ******************************************
const updatedUser = await UserModel.findByIdAndUpdate(
userId,
candidateUser,
{ new: true } // Returns updated user instead of pre-update user
)
.select("-password")
.select("-profileImage");
return updatedUser;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "updateUser";
throw error;
}
};
/**
* Delete a user by ID
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<UserModel>}
* @throws {Error}
*/
const deleteUser = async (userId) => {
const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
try {
const deletedUser = await UserModel.findByIdAndDelete(userId);
if (!deletedUser) {
throw new Error(stringService.dbUserNotFound);
editUserById = async (userId, user) => {
try {
await this.User.findByIdAndUpdate(userId, user, { new: true }).select("-password").select("-profileImage");
} catch (error) {
error.service = SERVICE_NAME;
error.method = "editUserById";
throw error;
}
return deletedUser;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteUser";
throw error;
}
};
};
}
/**
* Delete a user by ID
* @async
* @param {string} teamId
* @returns {void}
* @throws {Error}
*/
const deleteTeam = async (teamId) => {
try {
await TeamModel.findByIdAndDelete(teamId);
return true;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteTeam";
throw error;
}
};
const deleteAllOtherUsers = async () => {
try {
await UserModel.deleteMany({ role: { $ne: "superadmin" } });
return true;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteAllOtherUsers";
throw error;
}
};
const getAllUsers = async () => {
try {
const users = await UserModel.find().select("-password").select("-profileImage");
return users;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getAllUsers";
throw error;
}
};
const logoutUser = async (userId) => {
try {
await UserModel.updateOne({ _id: userId }, { $unset: { authToken: null } });
return true;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "logoutUser";
throw error;
}
};
const getUserById = async (roles, userId) => {
try {
if (!roles.includes("superadmin")) {
throw new Error("User is not a superadmin");
}
const user = await UserModel.findById(userId).select("-password").select("-profileImage");
if (!user) {
throw new Error("User not found");
}
return user;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getUserById";
throw error;
}
};
const editUserById = async (userId, user) => {
try {
await UserModel.findByIdAndUpdate(userId, user, { new: true }).select("-password").select("-profileImage");
} catch (error) {
error.service = SERVICE_NAME;
error.method = "editUserById";
throw error;
}
};
export {
checkSuperadmin,
insertUser,
getUserByEmail,
updateUser,
deleteUser,
deleteTeam,
deleteAllOtherUsers,
getAllUsers,
logoutUser,
getUserById,
editUserById,
};
export default UserModule;

View File

@@ -29,7 +29,7 @@ class UserService {
registerUser = async (user, file) => {
// Create a new user
// If superAdmin exists, a token should be attached to all further register requests
const superAdminExists = await this.db.checkSuperadmin();
const superAdminExists = await this.db.userModule.checkSuperadmin();
if (superAdminExists) {
const invitedUser = await this.db.inviteModule.getInviteTokenAndDelete(user.inviteToken);
user.role = invitedUser.role;
@@ -40,7 +40,7 @@ class UserService {
await this.db.updateAppSettings({ jwtSecret });
}
const newUser = await this.db.insertUser({ ...user }, file);
const newUser = await this.db.userModule.insertUser({ ...user }, file);
this.logger.debug({
message: "New user created",
@@ -83,7 +83,7 @@ class UserService {
loginUser = async (email, password) => {
// Check if user exists
const user = await this.db.getUserByEmail(email);
const user = await this.db.userModule.getUserByEmail(email);
// Compare password
const match = await user.comparePassword(password);
if (match !== true) {
@@ -110,7 +110,7 @@ class UserService {
// Add user email to body for DB operation
updates.email = currentUser.email;
// Get user
const user = await this.db.getUserByEmail(currentUser.email);
const user = await this.db.userModule.getUserByEmail(currentUser.email);
// Compare passwords
const match = await user.comparePassword(updates?.password);
// If not a match, throw a 403
@@ -122,17 +122,17 @@ class UserService {
updates.password = updates.newPassword;
}
const updatedUser = await this.db.updateUser({ userId: currentUser?._id, user: updates, file: file });
const updatedUser = await this.db.userModule.updateUser({ userId: currentUser?._id, user: updates, file: file });
return updatedUser;
};
checkSuperadminExists = async () => {
const superAdminExists = await this.db.checkSuperadmin();
const superAdminExists = await this.db.userModule.checkSuperadmin();
return superAdminExists;
};
requestRecovery = async (email) => {
const user = await this.db.getUserByEmail(email);
const user = await this.db.userModule.getUserByEmail(email);
const recoveryToken = await this.db.requestRecoveryToken(email);
const name = user.firstName;
const { clientHost } = this.settingsService.getSettings();
@@ -195,21 +195,21 @@ class UserService {
));
}
// 6. Delete the user by id
await this.db.deleteUser(userId);
await this.db.userModule.deleteUser(userId);
};
getAllUsers = async () => {
const users = await this.db.getAllUsers();
const users = await this.db.userModule.getAllUsers();
return users;
};
getUserById = async (roles, userId) => {
const user = await this.db.getUserById(roles, userId);
const user = await this.db.userModule.getUserById(roles, userId);
return user;
};
editUserById = async (userId, user) => {
await this.db.editUserById(userId, user);
await this.db.userModule.editUserById(userId, user);
};
}
export default UserService;