Merge branch 'develop' into feat/fe/resolve-monitor-incidents

This commit is contained in:
Br0wnHammer
2025-07-24 02:15:00 +05:30
33 changed files with 864 additions and 341 deletions
+268 -257
View File
@@ -3,117 +3,98 @@ import {
loginValidation,
editUserBodyValidation,
recoveryValidation,
recoveryTokenValidation,
recoveryTokenBodyValidation,
newPasswordValidation,
getUserByIdParamValidation,
editUserByIdParamValidation,
editUserByIdBodyValidation,
editSuperadminUserByIdBodyValidation,
} from "../validation/joi.js";
import jwt from "jsonwebtoken";
import { getTokenFromHeaders } from "../utils/utils.js";
import crypto from "crypto";
import { asyncHandler, createAuthError, createError } from "../utils/errorUtils.js";
import { asyncHandler, createError } from "../utils/errorUtils.js";
const SERVICE_NAME = "authController";
/**
* Authentication Controller
*
* Handles all authentication-related HTTP requests including user registration,
* login, password recovery, and user management operations.
*
* @class AuthController
* @description Manages user authentication and authorization operations
*/
class AuthController {
constructor({ db, settingsService, emailService, jobQueue, stringService, logger }) {
/**
* Creates an instance of AuthController.
*
* @param {Object} dependencies - The dependencies required by the controller
* @param {Object} dependencies.db - Database service for data operations
* @param {Object} dependencies.settingsService - Service for application settings
* @param {Object} dependencies.emailService - Service for email operations
* @param {Object} dependencies.jobQueue - Service for job queue operations
* @param {Object} dependencies.stringService - Service for string/localization
* @param {Object} dependencies.logger - Logger service
* @param {Object} dependencies.userService - User business logic service
*/
constructor({ db, settingsService, emailService, jobQueue, stringService, logger, userService }) {
this.db = db;
this.settingsService = settingsService;
this.emailService = emailService;
this.jobQueue = jobQueue;
this.stringService = stringService;
this.logger = logger;
this.userService = userService;
}
/**
* Creates and returns JWT token with an arbitrary payload
* @function
* @param {Object} payload
* @param {Object} appSettings
* @returns {String}
* @throws {Error}
*/
issueToken = (payload, appSettings) => {
const tokenTTL = appSettings?.jwtTTL ?? "2h";
const tokenSecret = appSettings?.jwtSecret;
const payloadData = payload;
return jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL });
};
/**
* Registers a new user. If the user is the first account, a JWT secret is created. If not, an invite token is required.
* Registers a new user in the system.
*
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.inviteToken - The invite token for registration.
* @property {Object} req.file - The file object for the user's profile image.
* @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 creation of the user, the created user data, and a JWT token.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
* @function registerUser
* @param {Object} req - Express request object
* @param {Object} req.body - Request body containing user registration data
* @param {string} req.body.firstName - User's first name
* @param {string} req.body.lastName - User's last name
* @param {string} req.body.email - User's email address (will be converted to lowercase)
* @param {string} req.body.password - User's password
* @param {string} [req.body.inviteToken] - Invite token for registration (required if superadmin exists)
* @param {string} [req.body.teamId] - Team ID (auto-assigned if superadmin)
* @param {Array<string>} [req.body.role] - User roles (auto-assigned if superadmin)
* @param {Object} [req.file] - Profile image file uploaded via multer
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with user data and JWT token
* @throws {Error} 422 - Validation error if request body is invalid
* @throws {Error} 409 - Conflict if user already exists
* @example
* // Register first user (becomes superadmin)
* POST /auth/register
* {
* "firstName": "John",
* "lastName": "Doe",
* "email": "john@example.com",
* "password": "SecurePass123!"
* }
*
* // Register subsequent user (requires invite token)
* POST /auth/register
* {
* "firstName": "Jane",
* "lastName": "Smith",
* "email": "jane@example.com",
* "password": "SecurePass123!",
* "inviteToken": "abc123..."
* }
*/
registerUser = asyncHandler(
async (req, res, next) => {
async (req, res) => {
if (req.body?.email) {
req.body.email = req.body.email?.toLowerCase();
}
await registrationBodyValidation.validateAsync(req.body);
// Create a new user
const user = req.body;
// If superAdmin exists, a token should be attached to all further register requests
const superAdminExists = await this.db.checkSuperadmin(req, res);
if (superAdminExists) {
const invitedUser = await this.db.getInviteTokenAndDelete(user.inviteToken);
user.role = invitedUser.role;
user.teamId = invitedUser.teamId;
} else {
// This is the first account, create JWT secret to use if one is not supplied by env
const jwtSecret = crypto.randomBytes(64).toString("hex");
await this.db.updateAppSettings({ jwtSecret });
}
const newUser = await this.db.insertUser({ ...req.body }, req.file);
this.logger.info({
message: this.stringService.authCreateUser,
service: SERVICE_NAME,
details: newUser._id,
});
const userForToken = { ...newUser._doc };
delete userForToken.profileImage;
delete userForToken.avatarImage;
const appSettings = await this.settingsService.getSettings();
const token = this.issueToken(userForToken, appSettings);
try {
const html = await this.emailService.buildEmail("welcomeEmailTemplate", {
name: newUser.firstName,
});
this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error) => {
this.logger.warn({
message: error.message,
service: SERVICE_NAME,
method: "registerUser",
stack: error.stack,
});
});
} catch (error) {
this.logger.warn({
message: error.message,
service: SERVICE_NAME,
method: "registerUser",
stack: error.stack,
});
}
const { user, token } = await this.userService.registerUser(req.body, req.file);
res.success({
msg: this.stringService.authCreateUser,
data: { user: newUser, token: token },
data: { user, token },
});
},
SERVICE_NAME,
@@ -121,51 +102,38 @@ class AuthController {
);
/**
* Logs in a user by validating the user's credentials and issuing a JWT token.
* Authenticates a user and returns a JWT token.
*
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.email - The email of the user.
* @property {string} req.body.password - The 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 login of the user, the user data (without password and avatar image), and a JWT token.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422) or the password is incorrect.
* @function loginUser
* @param {Object} req - Express request object
* @param {Object} req.body - Request body containing login credentials
* @param {string} req.body.email - User's email address (will be converted to lowercase)
* @param {string} req.body.password - User's password
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with user data and JWT token
* @throws {Error} 422 - Validation error if request body is invalid
* @throws {Error} 401 - Unauthorized if credentials are incorrect
* @example
* POST /auth/login
* {
* "email": "john@example.com",
* "password": "SecurePass123!"
* }
*/
loginUser = asyncHandler(
async (req, res, next) => {
async (req, res) => {
if (req.body?.email) {
req.body.email = req.body.email?.toLowerCase();
}
await loginValidation.validateAsync(req.body);
const { email, password } = req.body;
// Check if user exists
const user = await this.db.getUserByEmail(email);
// Compare password
const match = await user.comparePassword(password);
if (match !== true) {
throw createAuthError(this.stringService.authIncorrectPassword);
}
// Remove password from user object. Should this be abstracted to DB layer?
const userWithoutPassword = { ...user._doc };
delete userWithoutPassword.password;
delete userWithoutPassword.avatarImage;
// Happy path, return token
const appSettings = await this.settingsService.getSettings();
const token = this.issueToken(userWithoutPassword, appSettings);
// reset avatar image
userWithoutPassword.avatarImage = user.avatarImage;
const { user, token } = await this.userService.loginUser(req.body.email, req.body.password);
return res.success({
msg: this.stringService.authLoginUser,
data: {
user: userWithoutPassword,
token: token,
user,
token,
},
});
},
@@ -174,46 +142,43 @@ class AuthController {
);
/**
* Edits a user's information. If the user wants to change their password, the current password is checked before updating to the new password.
* Updates the current user's profile information.
*
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.params - The parameters of the request.
* @property {string} req.params.userId - The ID of the user to be edited.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.password - The current password of the user.
* @property {string} req.body.newPassword - 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 update of the user, and the updated user data.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422), the user is unauthorized (401), or the password is incorrect (403).
* @function editUser
* @param {Object} req - Express request object
* @param {Object} req.body - Request body containing user update data
* @param {string} [req.body.firstName] - Updated first name
* @param {string} [req.body.lastName] - Updated last name
* @param {string} [req.body.password] - Current password (required for password change)
* @param {string} [req.body.newPassword] - New password (required for password change)
* @param {boolean} [req.body.deleteProfileImage] - Flag to delete profile image
* @param {Object} [req.file] - New profile image file
* @param {Object} req.user - Current authenticated user (from JWT)
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with updated user data
* @throws {Error} 422 - Validation error if request body is invalid
* @throws {Error} 403 - Forbidden if current password is incorrect
* @example
* PUT /auth/user
* {
* "firstName": "John Updated",
* "lastName": "Doe Updated"
* }
*
* // Change password
* PUT /auth/user
* {
* "password": "OldPass123!",
* "newPassword": "NewPass123!"
* }
*/
editUser = asyncHandler(
async (req, res, next) => {
async (req, res) => {
await editUserBodyValidation.validateAsync(req.body);
// Change Password check
if (req.body.password && req.body.newPassword) {
// Get token from headers
const token = getTokenFromHeaders(req.headers);
// Get email from token
const { jwtSecret } = this.settingsService.getSettings();
const { email } = jwt.verify(token, jwtSecret);
// Add user email to body for DB operation
req.body.email = email;
// Get user
const user = await this.db.getUserByEmail(email);
// Compare passwords
const match = await user.comparePassword(req.body.password);
// If not a match, throw a 403
// 403 instead of 401 to avoid triggering axios interceptor
if (!match) {
throw createError(this.stringService.authIncorrectPassword, 403);
}
// If a match, update the password
req.body.password = req.body.newPassword;
}
const updatedUser = await this.userService.editUser(req.body, req.file, req.user);
const updatedUser = await this.db.updateUser({ userId: req?.user?._id, user: req.body, file: req.file });
res.success({
msg: this.stringService.authUpdateUser,
data: updatedUser,
@@ -224,17 +189,20 @@ class AuthController {
);
/**
* Checks if a superadmin account exists in the database.
* Checks if a superadmin account exists in the system.
*
* @async
* @param {Object} req - The Express request object.
* @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 existence of a superadmin, and a boolean indicating the existence of a superadmin.
* @throws {Error} If there is an error during the process.
* @function checkSuperadminExists
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with boolean indicating superadmin existence
* @example
* GET /auth/users/superadmin
* // Response: { "data": true } or { "data": false }
*/
checkSuperadminExists = asyncHandler(
async (req, res, next) => {
const superAdminExists = await this.db.checkSuperadmin(req, res);
async (req, res) => {
const superAdminExists = await this.userService.checkSuperadminExists();
return res.success({
msg: this.stringService.authAdminExists,
data: superAdminExists,
@@ -243,34 +211,30 @@ class AuthController {
SERVICE_NAME,
"checkSuperadminExists"
);
/**
* Requests a recovery token for a user. The user's email is validated and a recovery token is created and sent via email.
* Initiates password recovery process by sending a recovery email.
*
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.email - The email of the user requesting recovery.
* @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 creation of the recovery token, and the message ID of the sent email.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
* @function requestRecovery
* @param {Object} req - Express request object
* @param {Object} req.body - Request body containing email
* @param {string} req.body.email - Email address for password recovery
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with message ID
* @throws {Error} 422 - Validation error if email is invalid
* @throws {Error} 404 - Not found if user doesn't exist
* @example
* POST /auth/recovery/request
* {
* "email": "john@example.com"
* }
*/
requestRecovery = asyncHandler(
async (req, res, next) => {
async (req, res) => {
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,
@@ -279,21 +243,29 @@ class AuthController {
SERVICE_NAME,
"requestRecovery"
);
/**
* Validates a recovery token. The recovery token is validated and if valid, a success message is returned.
* Validates a password recovery token.
*
* @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).
* @function validateRecovery
* @param {Object} req - Express request object
* @param {Object} req.body - Request body containing recovery token
* @param {string} req.body.recoveryToken - Recovery token to validate
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response if token is valid
* @throws {Error} 422 - Validation error if token format is invalid
* @throws {Error} 400 - Bad request if token is invalid or expired
* @example
* POST /auth/recovery/validate
* {
* "recoveryToken": "abc123..."
* }
*/
validateRecovery = asyncHandler(
async (req, res, next) => {
await recoveryTokenValidation.validateAsync(req.body);
await this.db.validateRecoveryToken(req, res);
async (req, res) => {
await recoveryTokenBodyValidation.validateAsync(req.body);
await this.userService.validateRecovery(req.body.recoveryToken);
return res.success({
msg: this.stringService.authVerifyRecoveryToken,
});
@@ -303,23 +275,29 @@ class AuthController {
);
/**
* 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.
* Resets user password using a valid recovery token.
*
* @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).
* @function resetPassword
* @param {Object} req - Express request object
* @param {Object} req.body - Request body containing new password and recovery token
* @param {string} req.body.password - New password
* @param {string} req.body.recoveryToken - Valid recovery token
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with user data and JWT token
* @throws {Error} 422 - Validation error if password format is invalid
* @throws {Error} 400 - Bad request if token is invalid or expired
* @example
* POST /auth/recovery/reset
* {
* "password": "NewSecurePass123!",
* "recoveryToken": "abc123..."
* }
*/
resetPassword = asyncHandler(
async (req, res, next) => {
async (req, res) => {
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 },
@@ -330,54 +308,27 @@ class AuthController {
);
/**
* Deletes a user and all associated monitors, checks, and alerts.
* Deletes the current user's account and associated data.
*
* @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.
* @async
* @function deleteUser
* @param {Object} req - Express request object
* @param {Object} req.user - Current authenticated user (from JWT)
* @param {string} req.user._id - User ID
* @param {string} req.user.email - User email
* @param {string} req.user.teamId - User's team ID
* @param {Array<string>} req.user.role - User roles
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response confirming user deletion
* @throws {Error} 400 - Bad request if user is demo user
* @throws {Error} 404 - Not found if user doesn't exist
* @example
* DELETE /auth/user
* // Requires JWT authentication
*/
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);
async (req, res) => {
await this.userService.deleteUser(req.user);
return res.success({
msg: this.stringService.authDeleteUser,
});
@@ -386,9 +337,22 @@ class AuthController {
"deleteUser"
);
/**
* Retrieves all users in the system (admin/superadmin only).
*
* @async
* @function getAllUsers
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with array of users
* @throws {Error} 403 - Forbidden if user doesn't have admin/superadmin role
* @example
* GET /auth/users
* // Requires JWT authentication with admin/superadmin role
*/
getAllUsers = asyncHandler(
async (req, res, next) => {
const allUsers = await this.db.getAllUsers(req, res);
async (req, res) => {
const allUsers = await this.userService.getAllUsers();
return res.success({
msg: this.stringService.authGetAllUsers,
data: allUsers,
@@ -398,8 +362,27 @@ class AuthController {
"getAllUsers"
);
/**
* Retrieves a specific user by ID (superadmin only).
*
* @async
* @function getUserById
* @param {Object} req - Express request object
* @param {Object} req.params - URL parameters
* @param {string} req.params.userId - ID of the user to retrieve
* @param {Object} req.user - Current authenticated user (from JWT)
* @param {Array<string>} req.user.role - Current user's roles
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with user data
* @throws {Error} 422 - Validation error if userId is invalid
* @throws {Error} 403 - Forbidden if user doesn't have superadmin role
* @throws {Error} 404 - Not found if user doesn't exist
* @example
* GET /auth/users/507f1f77bcf86cd799439011
* // Requires JWT authentication with superadmin role
*/
getUserById = asyncHandler(
async (req, res, next) => {
async (req, res) => {
await getUserByIdParamValidation.validateAsync(req.params);
const userId = req?.params?.userId;
const roles = req?.user?.role;
@@ -412,7 +395,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 });
},
@@ -420,8 +403,36 @@ class AuthController {
"getUserById"
);
/**
* Updates a specific user by ID (superadmin only).
*
* @async
* @function editUserById
* @param {Object} req - Express request object
* @param {Object} req.params - URL parameters
* @param {string} req.params.userId - ID of the user to update
* @param {Object} req.body - Request body containing user update data
* @param {string} [req.body.firstName] - Updated first name
* @param {string} [req.body.lastName] - Updated last name
* @param {Array<string>} [req.body.role] - Updated user roles
* @param {Object} req.user - Current authenticated user (from JWT)
* @param {string} req.user._id - Current user's ID
* @param {Array<string>} req.user.role - Current user's roles
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response confirming user update
* @throws {Error} 422 - Validation error if parameters or body are invalid
* @throws {Error} 403 - Forbidden if user doesn't have superadmin role
* @throws {Error} 404 - Not found if user doesn't exist
* @example
* PUT /auth/users/507f1f77bcf86cd799439011
* {
* "firstName": "Updated Name",
* "role": ["admin"]
* }
* // Requires JWT authentication with superadmin role
*/
editUserById = asyncHandler(
async (req, res, next) => {
async (req, res) => {
const roles = req?.user?.role;
if (!roles.includes("superadmin")) {
throw createError("Unauthorized", 403);
@@ -438,7 +449,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,
-7
View File
@@ -138,13 +138,6 @@ class MongoDB {
});
}
};
checkSuperadmin = async (req, res) => {
const superAdmin = await UserModel.findOne({ role: "superadmin" });
if (superAdmin !== null) {
return true;
}
return false;
};
}
export default MongoDB;
+2 -2
View File
@@ -1,7 +1,7 @@
import InviteToken from "../../models/InviteToken.js";
import crypto from "crypto";
import ServiceRegistry from "../../../service/serviceRegistry.js";
import StringService from "../../../service/stringService.js";
import ServiceRegistry from "../../../service/system/serviceRegistry.js";
import StringService from "../../../service/system/stringService.js";
const SERVICE_NAME = "inviteModule";
/**
+2 -2
View File
@@ -4,8 +4,8 @@ import Check from "../../models/Check.js";
import PageSpeedCheck from "../../models/PageSpeedCheck.js";
import HardwareCheck from "../../models/HardwareCheck.js";
import { NormalizeData, NormalizeDataUptimeDetails } from "../../../utils/dataUtils.js";
import ServiceRegistry from "../../../service/serviceRegistry.js";
import StringService from "../../../service/stringService.js";
import ServiceRegistry from "../../../service/system/serviceRegistry.js";
import StringService from "../../../service/system/stringService.js";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
+10 -12
View File
@@ -1,25 +1,24 @@
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";
import serviceRegistry from "../../../service/system/serviceRegistry.js";
import StringService from "../../../service/system/stringService.js";
const SERVICE_NAME = "recoveryModule";
/**
* Request a recovery token
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @param {string} email
* @returns {Promise<UserModel>}
* @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 +30,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,13 +48,13 @@ 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);
const recoveryToken = await validateRecoveryToken(candidateToken);
const user = await UserModel.findOne({ email: recoveryToken.email });
if (user === null) {
+2 -2
View File
@@ -1,7 +1,7 @@
import StatusPage from "../../models/StatusPage.js";
import { NormalizeData } from "../../../utils/dataUtils.js";
import ServiceRegistry from "../../../service/serviceRegistry.js";
import StringService from "../../../service/stringService.js";
import ServiceRegistry from "../../../service/system/serviceRegistry.js";
import StringService from "../../../service/system/stringService.js";
const SERVICE_NAME = "statusPageModule";
+24 -4
View File
@@ -4,10 +4,18 @@ 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/serviceRegistry.js";
import StringService from "../../../service/stringService.js";
import ServiceRegistry from "../../../service/system/serviceRegistry.js";
import StringService from "../../../service/system/stringService.js";
const SERVICE_NAME = "userModule";
const checkSuperadmin = async () => {
const superAdmin = await UserModel.findOne({ role: "superadmin" });
if (superAdmin !== null) {
return true;
}
return false;
};
/**
* Insert a User
* @async
@@ -187,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;
@@ -238,4 +246,16 @@ const editUserById = async (userId, user) => {
}
};
export { insertUser, getUserByEmail, updateUser, deleteUser, deleteTeam, deleteAllOtherUsers, getAllUsers, logoutUser, getUserById, editUserById };
export {
checkSuperadmin,
insertUser,
getUserByEmail,
updateUser,
deleteUser,
deleteTeam,
deleteAllOtherUsers,
getAllUsers,
logoutUser,
getUserById,
editUserById,
};
+30 -17
View File
@@ -1,6 +1,7 @@
import path from "path";
import fs from "fs";
import swaggerUi from "swagger-ui-express";
import jwt from "jsonwebtoken";
import express from "express";
import helmet from "helmet";
@@ -46,56 +47,58 @@ import DiagnosticRoutes from "./routes/diagnosticRoute.js";
import DiagnosticController from "./controllers/diagnosticController.js";
//JobQueue service and dependencies
import JobQueue from "./service/JobQueue/JobQueue.js";
import JobQueueHelper from "./service/JobQueue/JobQueueHelper.js";
import JobQueue from "./service/infrastructure/JobQueue/JobQueue.js";
import JobQueueHelper from "./service/infrastructure/JobQueue/JobQueueHelper.js";
import { Queue, Worker } from "bullmq";
import PulseQueue from "./service/PulseQueue/PulseQueue.js";
import PulseQueueHelper from "./service/PulseQueue/PulseQueueHelper.js";
import PulseQueue from "./service/infrastructure/PulseQueue/PulseQueue.js";
import PulseQueueHelper from "./service/infrastructure/PulseQueue/PulseQueueHelper.js";
import SuperSimpleQueue from "./service/SuperSimpleQueue/SuperSimpleQueue.js";
import SuperSimpleQueueHelper from "./service/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import SuperSimpleQueue from "./service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
import SuperSimpleQueueHelper from "./service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import UserService from "./service/business/userService.js";
//Network service and dependencies
import NetworkService from "./service/networkService.js";
import NetworkService from "./service/infrastructure/networkService.js";
import axios from "axios";
import ping from "ping";
import http from "http";
import Docker from "dockerode";
import net from "net";
// Email service and dependencies
import EmailService from "./service/emailService.js";
import EmailService from "./service/infrastructure/emailService.js";
import nodemailer from "nodemailer";
import pkg from "handlebars";
const { compile } = pkg;
import mjml2html from "mjml";
// Settings Service and dependencies
import SettingsService from "./service/settingsService.js";
import SettingsService from "./service/system/settingsService.js";
import AppSettings from "./db/models/AppSettings.js";
// Status Service and dependencies
import StatusService from "./service/statusService.js";
import StatusService from "./service/infrastructure/statusService.js";
// Notification Service and dependencies
import NotificationService from "./service/notificationService.js";
import NotificationUtils from "./service/notificationUtils.js";
import NotificationService from "./service/infrastructure/notificationService.js";
import NotificationUtils from "./service/infrastructure/notificationUtils.js";
// Buffer Service and dependencies
import BufferService from "./service/bufferService.js";
import BufferService from "./service/infrastructure/bufferService.js";
// Service Registry
import ServiceRegistry from "./service/serviceRegistry.js";
import ServiceRegistry from "./service/system/serviceRegistry.js";
import MongoDB from "./db/mongo/MongoDB.js";
// Redis Service and dependencies
import IORedis from "ioredis";
import RedisService from "./service/redisService.js";
import RedisService from "./service/data/redisService.js";
import TranslationService from "./service/translationService.js";
import TranslationService from "./service/system/translationService.js";
import languageMiddleware from "./middleware/languageMiddleware.js";
import StringService from "./service/stringService.js";
import StringService from "./service/system/stringService.js";
const SERVICE_NAME = "Server";
const SHUTDOWN_TIMEOUT = 1000;
@@ -176,6 +179,14 @@ const startApp = async () => {
});
const redisService = new RedisService({ Redis: IORedis, logger });
const userService = new UserService({
db,
emailService,
settingsService,
logger,
stringService,
jwt,
});
// const jobQueueHelper = new JobQueueHelper({
// redisService,
@@ -235,6 +246,7 @@ const startApp = async () => {
ServiceRegistry.register(NotificationService.SERVICE_NAME, notificationService);
ServiceRegistry.register(TranslationService.SERVICE_NAME, translationService);
ServiceRegistry.register(RedisService.SERVICE_NAME, redisService);
ServiceRegistry.register(UserService.SERVICE_NAME, userService);
await translationService.initialize();
@@ -255,6 +267,7 @@ const startApp = async () => {
jobQueue: ServiceRegistry.get(JobQueue.SERVICE_NAME),
stringService: ServiceRegistry.get(StringService.SERVICE_NAME),
logger: logger,
userService: ServiceRegistry.get(UserService.SERVICE_NAME),
});
const monitorController = new MonitorController(
+2 -2
View File
@@ -1,6 +1,6 @@
import logger from "../utils/logger.js";
import ServiceRegistry from "../service/serviceRegistry.js";
import StringService from "../service/stringService.js";
import ServiceRegistry from "../service/system/serviceRegistry.js";
import StringService from "../service/system/stringService.js";
const handleErrors = (error, req, res, next) => {
console.log("ERROR", error);
+3 -3
View File
@@ -1,9 +1,9 @@
import jwt from "jsonwebtoken";
const TOKEN_PREFIX = "Bearer ";
const SERVICE_NAME = "allowedRoles";
import ServiceRegistry from "../service/serviceRegistry.js";
import StringService from "../service/stringService.js";
import SettingsService from "../service/settingsService.js";
import ServiceRegistry from "../service/system/serviceRegistry.js";
import StringService from "../service/system/stringService.js";
import SettingsService from "../service/system/settingsService.js";
const isAllowed = (allowedRoles) => {
return (req, res, next) => {
+3 -3
View File
@@ -1,7 +1,7 @@
import jwt from "jsonwebtoken";
import ServiceRegistry from "../service/serviceRegistry.js";
import SettingsService from "../service/settingsService.js";
import StringService from "../service/stringService.js";
import ServiceRegistry from "../service/system/serviceRegistry.js";
import SettingsService from "../service/system/settingsService.js";
import StringService from "../service/system/stringService.js";
const SERVICE_NAME = "verifyJWT";
const TOKEN_PREFIX = "Bearer ";
+2 -2
View File
@@ -1,6 +1,6 @@
import logger from "../utils/logger.js";
import ServiceRegistry from "../service/serviceRegistry.js";
import StringService from "../service/stringService.js";
import ServiceRegistry from "../service/system/serviceRegistry.js";
import StringService from "../service/system/stringService.js";
import { ObjectId } from "mongodb";
const SERVICE_NAME = "verifyOwnership";
+93 -21
View File
@@ -1079,6 +1079,17 @@
"dev": true,
"license": "(Unlicense OR Apache-2.0)"
},
"node_modules/@trysound/sax": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
"integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==",
"license": "ISC",
"optional": true,
"peer": true,
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -2252,12 +2263,14 @@
}
},
"node_modules/css-tree": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz",
"integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==",
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"mdn-data": "2.12.2",
"mdn-data": "2.0.30",
"source-map-js": "^1.0.1"
},
"engines": {
@@ -4547,10 +4560,12 @@
}
},
"node_modules/mdn-data": {
"version": "2.12.2",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz",
"integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
"license": "CC0-1.0"
"version": "2.0.30",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
"license": "CC0-1.0",
"optional": true,
"peer": true
},
"node_modules/media-typer": {
"version": "0.3.0",
@@ -6381,6 +6396,59 @@
"postcss": "^8.4.32"
}
},
"node_modules/postcss-svgo/node_modules/commander": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
"integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
"license": "MIT",
"engines": {
"node": ">=16"
}
},
"node_modules/postcss-svgo/node_modules/css-tree": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz",
"integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==",
"license": "MIT",
"dependencies": {
"mdn-data": "2.12.2",
"source-map-js": "^1.0.1"
},
"engines": {
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
}
},
"node_modules/postcss-svgo/node_modules/mdn-data": {
"version": "2.12.2",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz",
"integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
"license": "CC0-1.0"
},
"node_modules/postcss-svgo/node_modules/svgo": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz",
"integrity": "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==",
"license": "MIT",
"dependencies": {
"commander": "^11.1.0",
"css-select": "^5.1.0",
"css-tree": "^3.0.1",
"css-what": "^6.1.0",
"csso": "^5.0.5",
"picocolors": "^1.1.1",
"sax": "^1.4.1"
},
"bin": {
"svgo": "bin/svgo.js"
},
"engines": {
"node": ">=16"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/svgo"
}
},
"node_modules/postcss-unique-selectors": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-7.0.4.tgz",
@@ -7421,24 +7489,26 @@
}
},
"node_modules/svgo": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz",
"integrity": "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz",
"integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"commander": "^11.1.0",
"@trysound/sax": "0.2.0",
"commander": "^7.2.0",
"css-select": "^5.1.0",
"css-tree": "^3.0.1",
"css-tree": "^2.3.1",
"css-what": "^6.1.0",
"csso": "^5.0.5",
"picocolors": "^1.1.1",
"sax": "^1.4.1"
"picocolors": "^1.0.0"
},
"bin": {
"svgo": "bin/svgo.js"
"svgo": "bin/svgo"
},
"engines": {
"node": ">=16"
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
@@ -7446,12 +7516,14 @@
}
},
"node_modules/svgo/node_modules/commander": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
"integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=16"
"node": ">= 10"
}
},
"node_modules/swagger-ui-dist": {
+208
View File
@@ -0,0 +1,208 @@
const SERVICE_NAME = "userService";
import { createAuthError, createError } from "../../utils/errorUtils.js";
class UserService {
static SERVICE_NAME = SERVICE_NAME;
constructor({ db, emailService, settingsService, logger, stringService, jwt }) {
this.db = db;
this.emailService = emailService;
this.settingsService = settingsService;
this.logger = logger;
this.stringService = stringService;
this.jwt = jwt;
}
issueToken = (payload, appSettings) => {
const tokenTTL = appSettings?.jwtTTL ?? "2h";
const tokenSecret = appSettings?.jwtSecret;
const payloadData = payload;
return this.jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL });
};
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();
if (superAdminExists) {
const invitedUser = await this.db.getInviteTokenAndDelete(user.inviteToken);
user.role = invitedUser.role;
user.teamId = invitedUser.teamId;
} else {
// This is the first account, create JWT secret to use if one is not supplied by env
const jwtSecret = crypto.randomBytes(64).toString("hex");
await this.db.updateAppSettings({ jwtSecret });
}
const newUser = await this.db.insertUser({ ...user }, file);
this.logger.debug({
message: "New user created",
service: SERVICE_NAME,
method: "registerUser",
details: newUser._id,
});
const userForToken = { ...newUser._doc };
delete userForToken.profileImage;
delete userForToken.avatarImage;
const appSettings = await this.settingsService.getSettings();
const token = this.issueToken(userForToken, appSettings);
try {
const html = await this.emailService.buildEmail("welcomeEmailTemplate", {
name: newUser.firstName,
});
this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error) => {
this.logger.warn({
message: error.message,
service: SERVICE_NAME,
method: "registerUser",
stack: error.stack,
});
});
} catch (error) {
this.logger.warn({
message: error.message,
service: SERVICE_NAME,
method: "registerUser",
stack: error.stack,
});
}
return { user: newUser, token };
};
loginUser = async (email, password) => {
// Check if user exists
const user = await this.db.getUserByEmail(email);
// Compare password
const match = await user.comparePassword(password);
if (match !== true) {
throw createAuthError(this.stringService.authIncorrectPassword);
}
// Remove password from user object. Should this be abstracted to DB layer?
const userWithoutPassword = { ...user._doc };
delete userWithoutPassword.password;
delete userWithoutPassword.avatarImage;
// Happy path, return token
const appSettings = await this.settingsService.getSettings();
const token = this.issueToken(userWithoutPassword, appSettings);
// reset avatar image
userWithoutPassword.avatarImage = user.avatarImage;
return { user: userWithoutPassword, token };
};
editUser = async (updates, file, currentUser) => {
// Change Password check
if (updates?.password && updates?.newPassword) {
// Get user's email
// Add user email to body for DB operation
updates.email = currentUser.email;
// Get user
const user = await this.db.getUserByEmail(currentUser.email);
// Compare passwords
const match = await user.comparePassword(updates?.password);
// If not a match, throw a 403
// 403 instead of 401 to avoid triggering axios interceptor
if (!match) {
throw createError(this.stringService.authIncorrectPassword, 403);
}
// If a match, update the password
updates.password = updates.newPassword;
}
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;
@@ -1,5 +1,5 @@
import MonitorStats from "../db/models/MonitorStats.js";
import { safelyParseFloat } from "../utils/dataUtils.js";
import MonitorStats from "../../db/models/MonitorStats.js";
import { safelyParseFloat } from "../../utils/dataUtils.js";
const SERVICE_NAME = "StatusService";
class StatusService {
@@ -1,5 +1,5 @@
const SERVICE_NAME = "ServiceRegistry";
import logger from "../utils/logger.js";
import logger from "../../utils/logger.js";
class ServiceRegistry {
static SERVICE_NAME = SERVICE_NAME;
constructor() {
+208
View File
@@ -0,0 +1,208 @@
const SERVICE_NAME = "userService";
import { createAuthError, createError } from "../utils/errorUtils.js";
class UserService {
static SERVICE_NAME = SERVICE_NAME;
constructor({ db, emailService, settingsService, logger, stringService, jwt }) {
this.db = db;
this.emailService = emailService;
this.settingsService = settingsService;
this.logger = logger;
this.stringService = stringService;
this.jwt = jwt;
}
issueToken = (payload, appSettings) => {
const tokenTTL = appSettings?.jwtTTL ?? "2h";
const tokenSecret = appSettings?.jwtSecret;
const payloadData = payload;
return this.jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL });
};
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();
if (superAdminExists) {
const invitedUser = await this.db.getInviteTokenAndDelete(user.inviteToken);
user.role = invitedUser.role;
user.teamId = invitedUser.teamId;
} else {
// This is the first account, create JWT secret to use if one is not supplied by env
const jwtSecret = crypto.randomBytes(64).toString("hex");
await this.db.updateAppSettings({ jwtSecret });
}
const newUser = await this.db.insertUser({ ...user }, file);
this.logger.debug({
message: "New user created",
service: SERVICE_NAME,
method: "registerUser",
details: newUser._id,
});
const userForToken = { ...newUser._doc };
delete userForToken.profileImage;
delete userForToken.avatarImage;
const appSettings = await this.settingsService.getSettings();
const token = this.issueToken(userForToken, appSettings);
try {
const html = await this.emailService.buildEmail("welcomeEmailTemplate", {
name: newUser.firstName,
});
this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error) => {
this.logger.warn({
message: error.message,
service: SERVICE_NAME,
method: "registerUser",
stack: error.stack,
});
});
} catch (error) {
this.logger.warn({
message: error.message,
service: SERVICE_NAME,
method: "registerUser",
stack: error.stack,
});
}
return { user: newUser, token };
};
loginUser = async (email, password) => {
// Check if user exists
const user = await this.db.getUserByEmail(email);
// Compare password
const match = await user.comparePassword(password);
if (match !== true) {
throw createAuthError(this.stringService.authIncorrectPassword);
}
// Remove password from user object. Should this be abstracted to DB layer?
const userWithoutPassword = { ...user._doc };
delete userWithoutPassword.password;
delete userWithoutPassword.avatarImage;
// Happy path, return token
const appSettings = await this.settingsService.getSettings();
const token = this.issueToken(userWithoutPassword, appSettings);
// reset avatar image
userWithoutPassword.avatarImage = user.avatarImage;
return { user: userWithoutPassword, token };
};
editUser = async (updates, file, currentUser) => {
// Change Password check
if (updates?.password && updates?.newPassword) {
// Get user's email
// Add user email to body for DB operation
updates.email = currentUser.email;
// Get user
const user = await this.db.getUserByEmail(currentUser.email);
// Compare passwords
const match = await user.comparePassword(updates?.password);
// If not a match, throw a 403
// 403 instead of 401 to avoid triggering axios interceptor
if (!match) {
throw createError(this.stringService.authIncorrectPassword, 403);
}
// If a match, update the password
updates.password = updates.newPassword;
}
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;
+4 -4
View File
@@ -58,8 +58,8 @@ const registrationBodyValidation = joi.object({
});
const editUserBodyValidation = joi.object({
firstName: nameValidation.required(),
lastName: nameValidation.required(),
firstName: nameValidation.optional(),
lastName: nameValidation.optional(),
profileImage: joi.any(),
newPassword: joi.string().min(8).pattern(passwordPattern),
password: joi.string().min(8).pattern(passwordPattern),
@@ -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,