Merge pull request #2668 from bluewave-labs/feat/user-service

feat: user service
This commit is contained in:
Alexander Holliday authored and GitHub committed 2025-07-23 13:08:33 -07:00
commit 9bb9265a19
12 files changed
+784 -420

No files matched your search

+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;
+7 -10
View File
@@ -1,6 +1,5 @@
import UserModel from "../../models/User.js";
import RecoveryToken from "../../models/RecoveryToken.js";
import crypto from "crypto";
import serviceRegistry from "../../../service/serviceRegistry.js";
import StringService from "../../../service/stringService.js";
@@ -9,17 +8,16 @@ const SERVICE_NAME = "recoveryModule";
/**
* Request a recovery token
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @param {string} email
* @returns {Promise<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 +29,9 @@ const requestRecoveryToken = async (req, res) => {
}
};
const validateRecoveryToken = async (req, res) => {
const validateRecoveryToken = async (candidateToken) => {
const stringService = serviceRegistry.get(StringService.SERVICE_NAME);
try {
const candidateToken = req.body.recoveryToken;
const recoveryToken = await RecoveryToken.findOne({
token: candidateToken,
});
@@ -50,10 +47,10 @@ const validateRecoveryToken = async (req, res) => {
}
};
const resetPassword = async (req, res) => {
const resetPassword = async (password, candidateToken) => {
const stringService = serviceRegistry.get(StringService.SERVICE_NAME);
try {
const newPassword = req.body.password;
const newPassword = password;
// Validate token again
const recoveryToken = await validateRecoveryToken(req, res);
+22 -2
View File
@@ -8,6 +8,14 @@ import ServiceRegistry from "../../../service/serviceRegistry.js";
import StringService from "../../../service/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,
};
+13
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";
@@ -56,6 +57,8 @@ import PulseQueueHelper from "./service/PulseQueue/PulseQueueHelper.js";
import SuperSimpleQueue from "./service/SuperSimpleQueue/SuperSimpleQueue.js";
import SuperSimpleQueueHelper from "./service/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import UserService from "./service/userService.js";
//Network service and dependencies
import NetworkService from "./service/networkService.js";
import axios from "axios";
@@ -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(
+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": {
@@ -31,11 +31,7 @@ class SuperSimpleQueueHelper {
throw new Error("No network response");
}
const {
monitor: updatedMonitor,
statusChanged,
prevStatus,
} = await this.statusService.updateStatus(networkResponse);
const { monitor: updatedMonitor, statusChanged, prevStatus } = await this.statusService.updateStatus(networkResponse);
this.notificationService
.handleNotifications({
+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;
+52 -38
View File
@@ -1,42 +1,56 @@
<!-- name, email, url -->
<mjml>
<mj-head>
<mj-font name="Roboto" href="https://fonts.googleapis.com/css?family=Roboto:300,500"></mj-font>
<mj-attributes>
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
<mj-text font-weight="300" font-size="16px" color="#616161" line-height="24px"></mj-text>
<mj-section padding="0px"></mj-section>
</mj-attributes>
</mj-head>
<mj-body>
<mj-section padding="20px 0">
<mj-column width="100%">
<mj-text align="left" font-size="10px">
Message from Checkmate Service
</mj-text>
</mj-column>
</mj-section>
<mj-section>
<mj-column width="100%">
<mj-text>
<p>Hello {{ name }}!</p>
<p>
You are receiving this email because a password reset request has been made for {{ email }}. Please use the link below on the site to
reset your password.
</p>
<a href="{{url}}">Reset Password</a>
<p>If you didn't request this, please ignore this email.</p>
<mj-head>
<mj-font
name="Roboto"
href="https://fonts.googleapis.com/css?family=Roboto:300,500"
></mj-font>
<mj-attributes>
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
<mj-text
font-weight="300"
font-size="16px"
color="#616161"
line-height="24px"
></mj-text>
<mj-section padding="0px"></mj-section>
</mj-attributes>
</mj-head>
<mj-body>
<mj-section padding="20px 0">
<mj-column width="100%">
<mj-text
align="left"
font-size="10px"
>
Message from Checkmate Service
</mj-text>
</mj-column>
</mj-section>
<mj-section>
<mj-column width="100%">
<mj-text>
<p>Hello {{ name }}!</p>
<p>
You are receiving this email because a password reset request has been made for {{ email }}. Please use the link below on the site to
reset your password.
</p>
<a href="{{url}}">Reset Password</a>
<p>If you didn't request this, please ignore this email.</p>
<p>Thank you.</p>
</mj-text>
</mj-column>
<mj-column width="100%">
<mj-divider border-width="1px" border-color="#E0E0E0"></mj-divider>
<mj-text font-size="12px">
<p>This email was sent by Checkmate.</p>
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
<p>Thank you.</p>
</mj-text>
</mj-column>
<mj-column width="100%">
<mj-divider
border-width="1px"
border-color="#E0E0E0"
></mj-divider>
<mj-text font-size="12px">
<p>This email was sent by Checkmate.</p>
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
+67 -41
View File
@@ -1,42 +1,68 @@
<mjml>
<mj-head>
<mj-font name="Roboto" href="https://fonts.googleapis.com/css?family=Roboto:300,500"></mj-font>
<mj-attributes>
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
<mj-text font-weight="300" font-size="16px" color="#616161" line-height="24px"></mj-text>
<mj-section padding="0px"></mj-section>
</mj-attributes>
</mj-head>
<mj-body>
<mj-section padding="20px 0">
<mj-column width="100%">
<mj-text align="left" font-size="10px">
Message from Checkmate Service
</mj-text>
</mj-column>
<mj-column width="45%" padding-top="20px">
<mj-text align="center" font-weight="500" padding="0px" font-size="18px" color="red">
{{ monitor }} is down
</mj-text>
<mj-divider border-width="2px" border-color="#616161"></mj-divider>
</mj-column>
</mj-section>
<mj-section>
<mj-column width="100%">
<mj-text>
<p>Hello {{ name }}!</p>
<p>We detected an incident on one of your monitors. Your service is currently down. We'll send a message to you once it is up again.</p>
<p><b>Monitor name:</b> {{ monitor }}</p>
<p><b>URL:</b> {{ url }}</p>
</mj-text>
</mj-column>
<mj-column width="100%">
<mj-divider border-width="1px" border-color="#E0E0E0"></mj-divider>
<mj-button background-color="#1570EF"> View incident details </mj-button>
<mj-text font-size="12px">
<p>This email was sent by Checkmate.</p>
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
<mj-head>
<mj-font
name="Roboto"
href="https://fonts.googleapis.com/css?family=Roboto:300,500"
></mj-font>
<mj-attributes>
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
<mj-text
font-weight="300"
font-size="16px"
color="#616161"
line-height="24px"
></mj-text>
<mj-section padding="0px"></mj-section>
</mj-attributes>
</mj-head>
<mj-body>
<mj-section padding="20px 0">
<mj-column width="100%">
<mj-text
align="left"
font-size="10px"
>
Message from Checkmate Service
</mj-text>
</mj-column>
<mj-column
width="45%"
padding-top="20px"
>
<mj-text
align="center"
font-weight="500"
padding="0px"
font-size="18px"
color="red"
>
{{ monitor }} is down
</mj-text>
<mj-divider
border-width="2px"
border-color="#616161"
></mj-divider>
</mj-column>
</mj-section>
<mj-section>
<mj-column width="100%">
<mj-text>
<p>Hello {{ name }}!</p>
<p>We detected an incident on one of your monitors. Your service is currently down. We'll send a message to you once it is up again.</p>
<p><b>Monitor name:</b> {{ monitor }}</p>
<p><b>URL:</b> {{ url }}</p>
</mj-text>
</mj-column>
<mj-column width="100%">
<mj-divider
border-width="1px"
border-color="#E0E0E0"
></mj-divider>
<mj-button background-color="#1570EF"> View incident details </mj-button>
<mj-text font-size="12px">
<p>This email was sent by Checkmate.</p>
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
+49 -35
View File
@@ -1,37 +1,51 @@
<!-- name -->
<mjml>
<mj-head>
<mj-font name="Roboto" href="https://fonts.googleapis.com/css?family=Roboto:300,500"></mj-font>
<mj-attributes>
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
<mj-text font-weight="300" font-size="16px" color="#616161" line-height="24px"></mj-text>
<mj-section padding="0px"></mj-section>
</mj-attributes>
</mj-head>
<mj-body>
<mj-section padding="20px 0">
<mj-column width="100%">
<mj-text align="left" font-size="10px">
Message from Checkmate Service
</mj-text>
</mj-column>
</mj-section>
<mj-section>
<mj-column width="100%">
<mj-text>
<p>Hello {{ name }}!</p>
<p>Thank you for trying out Checkmate! We developed it with great care to meet our own needs, and we're excited to share it with you.</p>
<p>Checkmate is an automated way of checking whether a service such as a website or an application is available or not.</p>
<p>We hope you find our service as valuable as we do.</p>
<p>Thank you.</p>
</mj-text>
</mj-column>
<mj-column width="100%">
<mj-divider border-width="1px" border-color="#E0E0E0"></mj-divider>
<mj-text font-size="12px">
<p>This email was sent by Checkmate.</p>
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
<mj-head>
<mj-font
name="Roboto"
href="https://fonts.googleapis.com/css?family=Roboto:300,500"
></mj-font>
<mj-attributes>
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
<mj-text
font-weight="300"
font-size="16px"
color="#616161"
line-height="24px"
></mj-text>
<mj-section padding="0px"></mj-section>
</mj-attributes>
</mj-head>
<mj-body>
<mj-section padding="20px 0">
<mj-column width="100%">
<mj-text
align="left"
font-size="10px"
>
Message from Checkmate Service
</mj-text>
</mj-column>
</mj-section>
<mj-section>
<mj-column width="100%">
<mj-text>
<p>Hello {{ name }}!</p>
<p>Thank you for trying out Checkmate! We developed it with great care to meet our own needs, and we're excited to share it with you.</p>
<p>Checkmate is an automated way of checking whether a service such as a website or an application is available or not.</p>
<p>We hope you find our service as valuable as we do.</p>
<p>Thank you.</p>
</mj-text>
</mj-column>
<mj-column width="100%">
<mj-divider
border-width="1px"
border-color="#E0E0E0"
></mj-divider>
<mj-text font-size="12px">
<p>This email was sent by Checkmate.</p>
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
+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,