diff --git a/server/controllers/authController.js b/server/controllers/authController.js index bf38430a7..da0f2c8f7 100755 --- a/server/controllers/authController.js +++ b/server/controllers/authController.js @@ -3,7 +3,7 @@ import { loginValidation, editUserBodyValidation, recoveryValidation, - recoveryTokenValidation, + recoveryTokenBodyValidation, newPasswordValidation, getUserByIdParamValidation, editUserByIdParamValidation, @@ -93,20 +93,8 @@ class AuthController { requestRecovery = asyncHandler( async (req, res, next) => { await recoveryValidation.validateAsync(req.body); - const { email } = req.body; - const user = await this.db.getUserByEmail(email); - const recoveryToken = await this.db.requestRecoveryToken(req, res); - const name = user.firstName; - const { clientHost } = this.settingsService.getSettings(); - const url = `${clientHost}/set-new-password/${recoveryToken.token}`; - - const html = await this.emailService.buildEmail("passwordResetTemplate", { - name, - email, - url, - }); - const msgId = await this.emailService.sendEmail(email, "Checkmate Password Reset", html); - + const email = req?.body?.email; + const msgId = await this.userService.requestRecovery(email); return res.success({ msg: this.stringService.authCreateRecoveryToken, data: msgId, @@ -115,21 +103,11 @@ class AuthController { SERVICE_NAME, "requestRecovery" ); - /** - * Validates a recovery token. The recovery token is validated and if valid, a success message is returned. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.body - The body of the request. - * @property {string} req.body.token - The recovery token to be validated. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status and a message indicating the validation of the recovery token. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422). - */ + validateRecovery = asyncHandler( async (req, res, next) => { - await recoveryTokenValidation.validateAsync(req.body); - await this.db.validateRecoveryToken(req, res); + await recoveryTokenBodyValidation.validateAsync(req.body); + await this.userService.validateRecovery(req.body.recoveryToken); return res.success({ msg: this.stringService.authVerifyRecoveryToken, }); @@ -138,24 +116,10 @@ class AuthController { "validateRecovery" ); - /** - * Resets a user's password. The new password is validated and if valid, the user's password is updated in the database and a new JWT token is issued. - * @async - * @param {Object} req - The Express request object. - * @property {Object} req.body - The body of the request. - * @property {string} req.body.token - The recovery token. - * @property {string} req.body.password - The new password of the user. - * @param {Object} res - The Express response object. - * @param {function} next - The next middleware function. - * @returns {Object} The response object with a success status, a message indicating the reset of the password, the updated user data (without password and avatar image), and a new JWT token. - * @throws {Error} If there is an error during the process, especially if there is a validation error (422). - */ resetPassword = asyncHandler( async (req, res, next) => { await newPasswordValidation.validateAsync(req.body); - const user = await this.db.resetPassword(req, res); - const appSettings = await this.settingsService.getSettings(); - const token = this.issueToken(user._doc, appSettings); + const { user, token } = await this.userService.resetPassword(req.body.password, req.body.recoveryToken); return res.success({ msg: this.stringService.authResetPassword, data: { user, token }, @@ -165,55 +129,9 @@ class AuthController { "resetPassword" ); - /** - * Deletes a user and all associated monitors, checks, and alerts. - * - * @param {Object} req - The request object. - * @param {Object} res - The response object. - * @param {Function} next - The next middleware function. - * @returns {Object} The response object with success status and message. - * @throws {Error} If user validation fails or user is not found in the database. - */ deleteUser = asyncHandler( async (req, res, next) => { - const email = req?.user?.email; - if (!email) { - throw new Error("No email in request"); - } - - const teamId = req?.user?.teamId; - const userId = req?.user?._id; - - if (!teamId) { - throw new Error("No team ID in request"); - } - - if (!userId) { - throw new Error("No user ID in request"); - } - - const roles = req.user.role; - if (roles.includes("demo")) { - throw new Error("Demo user cannot be deleted"); - } - - // 1. Find all the monitors associated with the team ID if superadmin - const result = await this.db.getMonitorsByTeamId({ - teamId: teamId, - }); - - if (roles.includes("superadmin")) { - // 2. Remove all jobs, delete checks and alerts - result?.monitors.length > 0 && - (await Promise.all( - result.monitors.map(async (monitor) => { - await this.jobQueue.deleteJob(monitor); - }) - )); - } - // 6. Delete the user by id - await this.db.deleteUser(userId); - + await this.userService.deleteUser(req.user); return res.success({ msg: this.stringService.authDeleteUser, }); @@ -224,7 +142,7 @@ class AuthController { getAllUsers = asyncHandler( async (req, res, next) => { - const allUsers = await this.db.getAllUsers(req, res); + const allUsers = await this.userService.getAllUsers(); return res.success({ msg: this.stringService.authGetAllUsers, data: allUsers, @@ -248,7 +166,7 @@ class AuthController { throw new Error("No roles in request"); } - const user = await this.db.getUserById(roles, userId); + const user = await this.userService.getUserById(roles, userId); return res.success({ msg: "ok", data: user }); }, @@ -274,7 +192,7 @@ class AuthController { await editUserByIdBodyValidation.validateAsync(req.body); } - await this.db.editUserById(userId, user); + await this.userService.editUserById(userId, user); return res.success({ msg: "ok" }); }, SERVICE_NAME, diff --git a/server/db/mongo/modules/recoveryModule.js b/server/db/mongo/modules/recoveryModule.js index 3b39e847c..3ea18cd8a 100755 --- a/server/db/mongo/modules/recoveryModule.js +++ b/server/db/mongo/modules/recoveryModule.js @@ -1,6 +1,5 @@ import UserModel from "../../models/User.js"; import RecoveryToken from "../../models/RecoveryToken.js"; -import crypto from "crypto"; import serviceRegistry from "../../../service/serviceRegistry.js"; import StringService from "../../../service/stringService.js"; @@ -9,17 +8,16 @@ const SERVICE_NAME = "recoveryModule"; /** * Request a recovery token * @async - * @param {Express.Request} req - * @param {Express.Response} res + * @param {string} email * @returns {Promise} * @throws {Error} */ -const requestRecoveryToken = async (req, res) => { +const requestRecoveryToken = async (email) => { try { // Delete any existing tokens - await RecoveryToken.deleteMany({ email: req.body.email }); + await RecoveryToken.deleteMany({ email }); let recoveryToken = new RecoveryToken({ - email: req.body.email, + email, token: crypto.randomBytes(32).toString("hex"), }); await recoveryToken.save(); @@ -31,10 +29,9 @@ const requestRecoveryToken = async (req, res) => { } }; -const validateRecoveryToken = async (req, res) => { +const validateRecoveryToken = async (candidateToken) => { const stringService = serviceRegistry.get(StringService.SERVICE_NAME); try { - const candidateToken = req.body.recoveryToken; const recoveryToken = await RecoveryToken.findOne({ token: candidateToken, }); @@ -50,10 +47,10 @@ const validateRecoveryToken = async (req, res) => { } }; -const resetPassword = async (req, res) => { +const resetPassword = async (password, candidateToken) => { const stringService = serviceRegistry.get(StringService.SERVICE_NAME); try { - const newPassword = req.body.password; + const newPassword = password; // Validate token again const recoveryToken = await validateRecoveryToken(req, res); diff --git a/server/db/mongo/modules/userModule.js b/server/db/mongo/modules/userModule.js index 0c4543499..a1f056157 100755 --- a/server/db/mongo/modules/userModule.js +++ b/server/db/mongo/modules/userModule.js @@ -195,7 +195,7 @@ const deleteAllOtherUsers = async () => { } }; -const getAllUsers = async (req, res) => { +const getAllUsers = async () => { try { const users = await UserModel.find().select("-password").select("-profileImage"); return users; diff --git a/server/service/userService.js b/server/service/userService.js index 6707c64dd..3e1bf285f 100644 --- a/server/service/userService.js +++ b/server/service/userService.js @@ -118,6 +118,91 @@ class UserService { const updatedUser = await this.db.updateUser({ userId: currentUser?._id, user: updates, file: file }); return updatedUser; }; -} + checkSuperadminExists = async () => { + const superAdminExists = await this.db.checkSuperadmin(); + return superAdminExists; + }; + + requestRecovery = async (email) => { + const user = await this.db.getUserByEmail(email); + const recoveryToken = await this.db.requestRecoveryToken(email); + const name = user.firstName; + const { clientHost } = this.settingsService.getSettings(); + const url = `${clientHost}/set-new-password/${recoveryToken.token}`; + + const html = await this.emailService.buildEmail("passwordResetTemplate", { + name, + email, + url, + }); + const msgId = await this.emailService.sendEmail(email, "Checkmate Password Reset", html); + return msgId; + }; + + validateRecovery = async (recoveryToken) => { + await this.db.validateRecoveryToken(recoveryToken); + }; + + resetPassword = async (password, recoveryToken) => { + const user = await this.db.resetPassword(password, recoveryToken); + const appSettings = await this.settingsService.getSettings(); + const token = this.issueToken(user._doc, appSettings); + return { user, token }; + }; + + deleteUser = async (user) => { + const email = user?.email; + if (!email) { + throw new Error("No email in request"); + } + + const teamId = user?.teamId; + const userId = user?._id; + + if (!teamId) { + throw new Error("No team ID in request"); + } + + if (!userId) { + throw new Error("No user ID in request"); + } + + const roles = user?.role; + if (roles.includes("demo")) { + throw new Error("Demo user cannot be deleted"); + } + + // 1. Find all the monitors associated with the team ID if superadmin + const result = await this.db.getMonitorsByTeamId({ + teamId: teamId, + }); + + if (roles.includes("superadmin")) { + // 2. Remove all jobs, delete checks and alerts + result?.monitors.length > 0 && + (await Promise.all( + result.monitors.map(async (monitor) => { + await this.jobQueue.deleteJob(monitor); + }) + )); + } + // 6. Delete the user by id + await this.db.deleteUser(userId); + }; + + getAllUsers = async () => { + const users = await this.db.getAllUsers(); + return users; + }; + + getUserById = async (roles, userId) => { + const user = await this.db.getUserById(roles, userId); + return user; + }; + + editUserById = async (userId, user) => { + await this.db.editUserById(userId, user); + }; +} export default UserService; diff --git a/server/validation/joi.js b/server/validation/joi.js index c141bbd34..41b6fa471 100755 --- a/server/validation/joi.js +++ b/server/validation/joi.js @@ -73,7 +73,7 @@ const recoveryValidation = joi.object({ .required(), }); -const recoveryTokenValidation = joi.object({ +const recoveryTokenBodyValidation = joi.object({ recoveryToken: joi.string().required(), }); @@ -688,7 +688,7 @@ export { loginValidation, registrationBodyValidation, recoveryValidation, - recoveryTokenValidation, + recoveryTokenBodyValidation, newPasswordValidation, inviteRoleValidation, inviteBodyValidation,