From 8adad64248902e85f0a0efb13ed2fa3deea0a8e5 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 30 Jul 2025 10:41:53 -0700 Subject: [PATCH] refactor module into class --- server/src/config/services.js | 9 +- server/src/db/mongo/MongoDB.js | 10 +- server/src/db/mongo/modules/userModule.js | 396 ++++++++------------- server/src/service/business/userService.js | 22 +- 4 files changed, 174 insertions(+), 263 deletions(-) diff --git a/server/src/config/services.js b/server/src/config/services.js index f5c9b3066..8ac7c72e2 100644 --- a/server/src/config/services.js +++ b/server/src/config/services.js @@ -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); diff --git a/server/src/db/mongo/MongoDB.js b/server/src/db/mongo/MongoDB.js index 9235892a8..158f68f7c 100755 --- a/server/src/db/mongo/MongoDB.js +++ b/server/src/db/mongo/MongoDB.js @@ -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); diff --git a/server/src/db/mongo/modules/userModule.js b/server/src/db/mongo/modules/userModule.js index 974f2d2ed..83b3b15f9 100755 --- a/server/src/db/mongo/modules/userModule.js +++ b/server/src/db/mongo/modules/userModule.js @@ -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} - * @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} - * @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} - * @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} - * @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; diff --git a/server/src/service/business/userService.js b/server/src/service/business/userService.js index 4e6a41ab0..04c14c477 100644 --- a/server/src/service/business/userService.js +++ b/server/src/service/business/userService.js @@ -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;