Merge branch 'develop' into feat/fe/resolve-monitor-incidents
This commit is contained in:
commit
b27eccec88
33 files changed
+864
-341
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -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";
|
||||
/**
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 ";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
Generated
+93
-21
@@ -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": {
|
||||
|
||||
@@ -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;
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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() {
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user