Merge remote-tracking branch 'upstream/develop' into develop

This commit is contained in:
Alex Holliday
2024-10-11 09:39:29 +08:00
15 changed files with 3272 additions and 637 deletions

2
Server/.gitignore vendored
View File

@@ -2,3 +2,5 @@ node_modules
.env
*.log
*.sh
.nyc_output
coverage

8
Server/.mocharc.js Normal file
View File

@@ -0,0 +1,8 @@
module.exports = {
require: ["chai/register-expect.js"], // Include Chai's "expect" interface globally
spec: "tests/**/*.test.js", // Specify test files
timeout: 5000, // Set test-case timeout in milliseconds
recursive: true, // Include subdirectories
reporter: "spec", // Use the "spec" reporter
exit: true, // Force Mocha to quit after tests complete
};

8
Server/.nycrc Normal file
View File

@@ -0,0 +1,8 @@
{
"all": true,
"include": ["**/*.js"],
"exclude": ["**/*.test.js"],
"reporter": ["html", "text", "lcov"],
"sourceMap": false,
"instrument": true
}

View File

@@ -1,19 +1,20 @@
const {
registrationBodyValidation,
loginValidation,
editUserParamValidation,
editUserBodyValidation,
recoveryValidation,
recoveryTokenValidation,
newPasswordValidation,
registrationBodyValidation,
loginValidation,
editUserParamValidation,
editUserBodyValidation,
recoveryValidation,
recoveryTokenValidation,
newPasswordValidation,
} = require("../validation/joi");
const logger = require("../utils/logger");
require("dotenv").config();
const {errorMessages, successMessages} = require("../utils/messages");
const { errorMessages, successMessages } = require("../utils/messages");
const jwt = require("jsonwebtoken");
const SERVICE_NAME = "AuthController";
const {getTokenFromHeaders} = require("../utils/utils");
const { getTokenFromHeaders } = require("../utils/utils");
const crypto = require("crypto");
const { handleValidationError, handleError } = require("./controllerUtils");
/**
* Creates and returns JWT token with an arbitrary payload
@@ -24,14 +25,12 @@ const crypto = require("crypto");
* @throws {Error}
*/
const issueToken = (payload, appSettings) => {
try {
const tokenTTL = appSettings.jwtTTL ? appSettings.jwtTTL : "2h";
return jwt.sign(payload, appSettings.jwtSecret, {expiresIn: tokenTTL});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "issueToken") : null;
throw error;
}
try {
const tokenTTL = appSettings.jwtTTL ? appSettings.jwtTTL : "2h";
return jwt.sign(payload, appSettings.jwtSecret, { expiresIn: tokenTTL });
} catch (error) {
throw handleError(error, SERVICE_NAME, "issueToken");
}
};
/**
@@ -47,56 +46,61 @@ const issueToken = (payload, appSettings) => {
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
const registerUser = async (req, res, next) => {
// joi validation
try {
await registrationBodyValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message = error.details?.[0]?.message || error.message || "Validation Error";
next(error);
return;
// joi validation
try {
await registrationBodyValidation.validateAsync(req.body);
} catch (error) {
const validationError = handleValidationError(error, SERVICE_NAME);
next(validationError);
return;
}
// Create a new user
try {
const { inviteToken } = req.body;
// If superAdmin exists, a token should be attached to all further register requests
const superAdminExists = await req.db.checkSuperadmin(req, res);
if (superAdminExists) {
await req.db.getInviteTokenAndDelete(inviteToken);
} 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 req.db.updateAppSettings({ jwtSecret });
}
// Create a new user
try {
const {inviteToken} = req.body;
// If superAdmin exists, a token should be attached to all further register requests
const superAdminExists = await req.db.checkSuperadmin(req, res);
if (superAdminExists) {
await req.db.getInviteTokenAndDelete(inviteToken);
} 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 req.db.updateAppSettings({jwtSecret});
}
const newUser = await req.db.insertUser({...req.body}, req.file);
logger.info(successMessages.AUTH_CREATE_USER, {
service: SERVICE_NAME, userId: newUser._id,
const newUser = await req.db.insertUser({ ...req.body }, req.file);
logger.info(successMessages.AUTH_CREATE_USER, {
service: SERVICE_NAME,
userId: newUser._id,
});
const userForToken = { ...newUser._doc };
delete userForToken.profileImage;
delete userForToken.avatarImage;
const appSettings = await req.settingsService.getSettings();
const token = issueToken(userForToken, appSettings);
req.emailService
.buildAndSendEmail(
"welcomeEmailTemplate",
{ name: newUser.firstName },
newUser.email,
"Welcome to Uptime Monitor"
)
.catch((error) => {
logger.error("Error sending welcome email", {
service: SERVICE_NAME,
error: error.message,
});
});
const userForToken = {...newUser._doc};
delete userForToken.profileImage;
delete userForToken.avatarImage;
const appSettings = await req.settingsService.getSettings();
const token = issueToken(userForToken, appSettings);
req.emailService
.buildAndSendEmail("welcomeEmailTemplate", {name: newUser.firstName}, newUser.email, "Welcome to Uptime Monitor")
.catch((error) => {
logger.error("Error sending welcome email", {
service: SERVICE_NAME, error: error.message,
});
});
return res.status(200).json({
success: true, msg: successMessages.AUTH_CREATE_USER, data: {user: newUser, token: token},
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "registerController") : null;
next(error);
}
return res.status(200).json({
success: true,
msg: successMessages.AUTH_CREATE_USER,
data: { user: newUser, token: token },
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "registerController"));
}
};
/**
@@ -112,47 +116,44 @@ const registerUser = async (req, res, next) => {
* @throws {Error} If there is an error during the process, especially if there is a validation error (422) or the password is incorrect.
*/
const loginUser = async (req, res, next) => {
try {
await loginValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message = error.details?.[0]?.message || error.message || "Validation Error";
next(error);
return;
try {
await loginValidation.validateAsync(req.body);
} catch (error) {
const validationError = handleValidationError(error, SERVICE_NAME);
next(validationError);
return;
}
try {
const { email, password } = req.body;
// Check if user exists
const user = await req.db.getUserByEmail(email);
// Compare password
const match = await user.comparePassword(password);
if (match !== true) {
next(new Error(errorMessages.AUTH_INCORRECT_PASSWORD));
return;
}
try {
const {email, password} = req.body;
// Check if user exists
const user = await req.db.getUserByEmail(email);
// Compare password
const match = await user.comparePassword(password);
if (match !== true) {
next(new Error(errorMessages.AUTH_INCORRECT_PASSWORD));
return;
}
// Remove password from user object. Should this be abstracted to DB layer?
const userWithoutPassword = { ...user._doc };
delete userWithoutPassword.password;
delete userWithoutPassword.avatarImage;
// 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 req.settingsService.getSettings();
const token = issueToken(userWithoutPassword, appSettings);
// reset avatar image
userWithoutPassword.avatarImage = user.avatarImage;
// Happy path, return token
const appSettings = req.settingsService.getSettings();
const token = issueToken(userWithoutPassword, appSettings);
// reset avatar image
userWithoutPassword.avatarImage = user.avatarImage;
return res.status(200).json({
success: true, msg: successMessages.AUTH_LOGIN_USER, data: {user: userWithoutPassword, token: token},
});
} catch (error) {
error.status = 500;
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "loginController") : null;
next(error);
}
return res.status(200).json({
success: true,
msg: successMessages.AUTH_LOGIN_USER,
data: { user: userWithoutPassword, token: token },
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "loginController"));
}
};
/**
@@ -170,60 +171,58 @@ const loginUser = async (req, res, next) => {
* @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).
*/
const editUser = async (req, res, next) => {
try {
await editUserParamValidation.validateAsync(req.params);
await editUserBodyValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message = error.details?.[0]?.message || error.message || "Validation Error";
try {
await editUserParamValidation.validateAsync(req.params);
await editUserBodyValidation.validateAsync(req.body);
} catch (error) {
const validationError = handleValidationError(error, SERVICE_NAME);
next(validationError);
return;
}
// TODO is this neccessary any longer? Verify ownership middleware should handle this
if (req.params.userId !== req.user._id.toString()) {
const error = new Error(errorMessages.AUTH_UNAUTHORIZED);
error.status = 401;
error.service = SERVICE_NAME;
next(error);
return;
}
try {
// 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 } = req.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 req.db.getUserByEmail(email);
// Compare passwords
const match = await user.comparePassword(req.body.password);
// If not a match, throw a 403
if (!match) {
const error = new Error(errorMessages.AUTH_INCORRECT_PASSWORD);
error.status = 403;
next(error);
return;
}
// If a match, update the password
req.body.password = req.body.newPassword;
}
// TODO is this neccessary any longer? Verify ownership middleware should handle this
if (req.params.userId !== req.user._id.toString()) {
const error = new Error(errorMessages.AUTH_UNAUTHORIZED);
error.status = 401;
error.service = SERVICE_NAME;
next(error);
return;
}
try {
// 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} = req.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 req.db.getUserByEmail(email);
// Compare passwords
const match = await user.comparePassword(req.body.password);
// If not a match, throw a 403
if (!match) {
const error = new Error(errorMessages.AUTH_INCORRECT_PASSWORD);
error.status = 403;
next(error)
return;
}
// If a match, update the password
req.body.password = req.body.newPassword;
}
const updatedUser = await req.db.updateUser(req, res);
return res.status(200).json({
success: true, msg: successMessages.AUTH_UPDATE_USER, data: updatedUser,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "userEditController") : null;
next(error);
}
const updatedUser = await req.db.updateUser(req, res);
return res.status(200).json({
success: true,
msg: successMessages.AUTH_UPDATE_USER,
data: updatedUser,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "userEditController"));
}
};
/**
@@ -236,16 +235,16 @@ const editUser = async (req, res, next) => {
* @throws {Error} If there is an error during the process.
*/
const checkSuperAdminExists = async (req, res, next) => {
try {
const superAdminExists = await req.db.checkSuperadmin(req, res);
return res.status(200).json({
success: true, msg: successMessages.AUTH_ADMIN_EXISTS, data: superAdminExists,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "checkSuperadminController") : null;
next(error);
}
try {
const superAdminExists = await req.db.checkSuperadmin(req, res);
return res.status(200).json({
success: true,
msg: successMessages.AUTH_ADMIN_EXISTS,
data: superAdminExists,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "checkSuperadminController"));
}
};
/**
@@ -260,41 +259,44 @@ const checkSuperAdminExists = async (req, res, next) => {
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
const requestRecovery = async (req, res, next) => {
try {
await recoveryValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message = error.details?.[0]?.message || error.message || "Validation Error";
next(error);
return;
}
try {
const {email} = req.body;
const user = await req.db.getUserByEmail(email);
if (user) {
const recoveryToken = await req.db.requestRecoveryToken(req, res);
const name = user.firstName;
const email = req.body.email;
const {clientHost} = req.settingsService.getSettings();
const url = `${clientHost}/set-new-password/${recoveryToken.token}`;
const msgId = await req.emailService.buildAndSendEmail("passwordResetTemplate", {
name,
email,
url
}, email, "Bluewave Uptime Password Reset");
return res.status(200).json({
success: true, msg: successMessages.AUTH_CREATE_RECOVERY_TOKEN, data: msgId,
});
}
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "recoveryRequestController") : null;
next(error);
try {
await recoveryValidation.validateAsync(req.body);
} catch (error) {
const validationError = handleValidationError(error, SERVICE_NAME);
next(validationError);
return;
}
try {
const { email } = req.body;
const user = await req.db.getUserByEmail(email);
if (user) {
const recoveryToken = await req.db.requestRecoveryToken(req, res);
const name = user.firstName;
const email = req.body.email;
const { clientHost } = req.settingsService.getSettings();
const url = `${clientHost}/set-new-password/${recoveryToken.token}`;
const msgId = await req.emailService.buildAndSendEmail(
"passwordResetTemplate",
{
name,
email,
url,
},
email,
"Bluewave Uptime Password Reset"
);
return res.status(200).json({
success: true,
msg: successMessages.AUTH_CREATE_RECOVERY_TOKEN,
data: msgId,
});
}
} catch (error) {
next(handleError(error, SERVICE_NAME, "recoveryRequestController"));
}
};
/**
@@ -309,26 +311,23 @@ const requestRecovery = async (req, res, next) => {
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
const validateRecovery = async (req, res, next) => {
try {
await recoveryTokenValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message = error.details?.[0]?.message || error.message || "Validation Error";
next(error);
return;
}
try {
await recoveryTokenValidation.validateAsync(req.body);
} catch (error) {
const validationError = handleValidationError(error, SERVICE_NAME);
next(validationError);
return;
}
try {
await req.db.validateRecoveryToken(req, res);
return res.status(200).json({
success: true, msg: successMessages.AUTH_VERIFY_RECOVERY_TOKEN,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "validateRecoveryTokenController") : null;
next(error);
}
try {
await req.db.validateRecoveryToken(req, res);
return res.status(200).json({
success: true,
msg: successMessages.AUTH_VERIFY_RECOVERY_TOKEN,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "validateRecoveryTokenController"));
}
};
/**
@@ -344,28 +343,26 @@ const validateRecovery = async (req, res, next) => {
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
const resetPassword = async (req, res, next) => {
try {
await newPasswordValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message = error.details?.[0]?.message || error.message || "Validation Error";
next(error);
return;
}
try {
const user = await req.db.resetPassword(req, res);
try {
await newPasswordValidation.validateAsync(req.body);
} catch (error) {
const validationError = handleValidationError(error, SERVICE_NAME);
next(validationError);
return;
}
try {
const user = await req.db.resetPassword(req, res);
const appSettings = await req.settingsService.getSettings();
const token = issueToken(user._doc, appSettings);
res.status(200).json({
success: true, msg: successMessages.AUTH_RESET_PASSWORD, data: {user, token},
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "resetPasswordController") : null;
next(error);
}
const appSettings = await req.settingsService.getSettings();
const token = issueToken(user._doc, appSettings);
res.status(200).json({
success: true,
msg: successMessages.AUTH_RESET_PASSWORD,
data: { user, token },
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "resetPasswordController"));
}
};
/**
@@ -378,73 +375,73 @@ const resetPassword = async (req, res, next) => {
* @throws {Error} If user validation fails or user is not found in the database.
*/
const deleteUser = async (req, res, next) => {
try {
const token = getTokenFromHeaders(req.headers);
const decodedToken = jwt.decode(token);
const {email} = decodedToken;
try {
const token = getTokenFromHeaders(req.headers);
const decodedToken = jwt.decode(token);
const { email } = decodedToken;
// Check if the user exists
const user = await req.db.getUserByEmail(email);
if (!user) {
next(new Error(errorMessages.DB_USER_NOT_FOUND));
return;
}
// 1. Find all the monitors associated with the team ID if superadmin
const result = await req.db.getMonitorsByTeamId({
params: {teamId: user.teamId},
});
if (user.role.includes("superadmin")) {
//2. Remove all jobs, delete checks and alerts
result?.monitors.length > 0 && (await Promise.all(result.monitors.map(async (monitor) => {
await req.jobQueue.deleteJob(monitor);
await req.db.deleteChecks(monitor._id);
await req.db.deletePageSpeedChecksByMonitorId(monitor._id);
await req.db.deleteNotificationsByMonitorId(monitor._id);
})));
// 3. Delete team
await req.db.deleteTeam(user.teamId);
// 4. Delete all other team members
await req.db.deleteAllOtherUsers();
// 5. Delete each monitor
await req.db.deleteMonitorsByUserId(user._id);
}
// 6. Delete the user by id
await req.db.deleteUser(user._id);
return res.status(200).json({
success: true, msg: successMessages.AUTH_DELETE_USER,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "deleteUserController") : null;
next(error);
// Check if the user exists
const user = await req.db.getUserByEmail(email);
if (!user) {
next(new Error(errorMessages.DB_USER_NOT_FOUND));
return;
}
// 1. Find all the monitors associated with the team ID if superadmin
const result = await req.db.getMonitorsByTeamId({
params: { teamId: user.teamId },
});
if (user.role.includes("superadmin")) {
//2. Remove all jobs, delete checks and alerts
result?.monitors.length > 0 &&
(await Promise.all(
result.monitors.map(async (monitor) => {
await req.jobQueue.deleteJob(monitor);
await req.db.deleteChecks(monitor._id);
await req.db.deletePageSpeedChecksByMonitorId(monitor._id);
await req.db.deleteNotificationsByMonitorId(monitor._id);
})
));
// 3. Delete team
await req.db.deleteTeam(user.teamId);
// 4. Delete all other team members
await req.db.deleteAllOtherUsers();
// 5. Delete each monitor
await req.db.deleteMonitorsByUserId(user._id);
}
// 6. Delete the user by id
await req.db.deleteUser(user._id);
return res.status(200).json({
success: true,
msg: successMessages.AUTH_DELETE_USER,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "deleteUserController"));
}
};
const getAllUsers = async (req, res) => {
try {
const allUsers = await req.db.getAllUsers(req, res);
res
.status(200)
.json({success: true, msg: "Got all users", data: allUsers});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "getAllUsersController") : null;
next(error);
}
try {
const allUsers = await req.db.getAllUsers(req, res);
res
.status(200)
.json({ success: true, msg: "Got all users", data: allUsers });
} catch (error) {
next(handleError(error, SERVICE_NAME, "getAllUsersController"));
}
};
module.exports = {
registerUser,
loginUser,
editUser,
checkSuperadminExists: checkSuperAdminExists,
requestRecovery,
validateRecovery,
resetPassword,
deleteUser,
getAllUsers,
registerUser,
loginUser,
editUser,
checkSuperadminExists: checkSuperAdminExists,
requestRecovery,
validateRecovery,
resetPassword,
deleteUser,
getAllUsers,
};

View File

@@ -13,17 +13,14 @@ const { successMessages } = require("../utils/messages");
const jwt = require("jsonwebtoken");
const { getTokenFromHeaders } = require("../utils/utils");
const SERVICE_NAME = "checkController";
const { handleValidationError, handleError } = require("./controllerUtils");
const createCheck = async (req, res, next) => {
try {
await createCheckParamValidation.validateAsync(req.params);
await createCheckBodyValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
@@ -34,9 +31,7 @@ const createCheck = async (req, res, next) => {
.status(200)
.json({ success: true, msg: successMessages.CHECK_CREATE, data: check });
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "createCheck") : null;
next(error);
next(handleError(error, SERVICE_NAME, "createCheck"));
}
};
@@ -45,11 +40,7 @@ const getChecks = async (req, res, next) => {
await getChecksParamValidation.validateAsync(req.params);
await getChecksQueryValidation.validateAsync(req.query);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
@@ -62,9 +53,7 @@ const getChecks = async (req, res, next) => {
data: { checksCount, checks },
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "getChecks") : null;
next(error);
next(handleError(error, SERVICE_NAME, "getChecks"));
}
};
@@ -73,11 +62,7 @@ const getTeamChecks = async (req, res, next) => {
await getTeamChecksParamValidation.validateAsync(req.params);
await getTeamChecksQueryValidation.validateAsync(req.query);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
@@ -88,9 +73,7 @@ const getTeamChecks = async (req, res, next) => {
data: checkData,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "getTeamChecks") : null;
next(error);
next(handleError(error, SERVICE_NAME, "getTeamChecks"));
}
};
@@ -98,11 +81,7 @@ const deleteChecks = async (req, res, next) => {
try {
await deleteChecksParamValidation.validateAsync(req.params);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
@@ -114,9 +93,7 @@ const deleteChecks = async (req, res, next) => {
data: { deletedCount },
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "deleteChecks") : null;
next(error);
next(handleError(error, SERVICE_NAME, "deleteChecks"));
}
};
@@ -124,10 +101,7 @@ const deleteChecksByTeamId = async (req, res, next) => {
try {
await deleteChecksByTeamIdParamValidation.validateAsync(req.params);
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "deleteChecksByTeam") : null;
error.status = 422;
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
@@ -139,9 +113,7 @@ const deleteChecksByTeamId = async (req, res, next) => {
data: { deletedCount },
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "deleteChecksByTeamId") : null;
next(error);
next(handleError(error, SERVICE_NAME, "deleteChecksByTeamId"));
}
};
@@ -151,12 +123,7 @@ const updateChecksTTL = async (req, res, next) => {
try {
await updateChecksTTLBodyValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "updateChecksTTL") : null;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
@@ -172,9 +139,7 @@ const updateChecksTTL = async (req, res, next) => {
msg: successMessages.CHECK_UPDATE_TTL,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "updateTTL") : null;
next(error);
next(handleError(error, SERVICE_NAME, "updateTTL"));
}
};

View File

@@ -0,0 +1,19 @@
const handleValidationError = (error, serviceName) => {
error.status = 422;
error.service = serviceName;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
return error;
};
const handleError = (error, serviceName, method, code = 500) => {
error.code === undefined ? (error.code = code) : null;
error.service === undefined ? (error.service = serviceName) : null;
error.method === undefined ? (error.method = method) : null;
return error;
};
module.exports = {
handleValidationError,
handleError,
};

View File

@@ -1,22 +1,23 @@
const {
inviteRoleValidation,
inviteBodyValidation,
inviteVerificationBodyValidation,
inviteRoleValidation,
inviteBodyValidation,
inviteVerificationBodyValidation,
} = require("../validation/joi");
const logger = require("../utils/logger");
require("dotenv").config();
const jwt = require("jsonwebtoken");
const { handleError, handleValidationError } = require("./controllerUtils");
const SERVICE_NAME = "inviteController";
const getTokenFromHeaders = (headers) => {
const authorizationHeader = headers.authorization;
if (!authorizationHeader) throw new Error("No auth headers");
const authorizationHeader = headers.authorization;
if (!authorizationHeader) throw new Error("No auth headers");
const parts = authorizationHeader.split(" ");
if (parts.length !== 2 || parts[0] !== "Bearer")
throw new Error("Invalid auth headers");
const parts = authorizationHeader.split(" ");
if (parts.length !== 2 || parts[0] !== "Bearer")
throw new Error("Invalid auth headers");
return parts[1];
return parts[1];
};
/**
@@ -33,79 +34,65 @@ const getTokenFromHeaders = (headers) => {
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
const issueInvitation = async (req, res, next) => {
try {
// Only admins can invite
const token = getTokenFromHeaders(req.headers);
const { role, firstname, teamId } = jwt.decode(token);
req.body.teamId = teamId;
try {
// Only admins can invite
const token = getTokenFromHeaders(req.headers);
const {role, firstname, teamId} = jwt.decode(token);
req.body.teamId = teamId;
try {
await inviteRoleValidation.validateAsync({roles: role});
await inviteBodyValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
return;
}
const inviteToken = await req.db.requestInviteToken({...req.body});
const {clientHost} = req.settingsService.getSettings();
req.emailService
.buildAndSendEmail(
"employeeActivationTemplate",
{
name: firstname,
link: `${clientHost}/register/${inviteToken.token}`,
},
req.body.email,
"Welcome to Uptime Monitor"
)
.catch((error) => {
logger.error("Error sending invite email", {
service: SERVICE_NAME,
error: error.message,
});
});
return res
.status(200)
.json({success: true, msg: "Invite sent", data: inviteToken});
await inviteRoleValidation.validateAsync({ roles: role });
await inviteBodyValidation.validateAsync(req.body);
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "inviteController") : null;
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
const inviteToken = await req.db.requestInviteToken({ ...req.body });
const { clientHost } = req.settingsService.getSettings();
req.emailService
.buildAndSendEmail(
"employeeActivationTemplate",
{
name: firstname,
link: `${clientHost}/register/${inviteToken.token}`,
},
req.body.email,
"Welcome to Uptime Monitor"
)
.catch((error) => {
logger.error("Error sending invite email", {
service: SERVICE_NAME,
error: error.message,
});
});
return res
.status(200)
.json({ success: true, msg: "Invite sent", data: inviteToken });
} catch (error) {
next(handleError(error, SERVICE_NAME, "inviteController"));
}
};
const inviteVerifyController = async (req, res, next) => {
try {
await inviteVerificationBodyValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
return;
}
try {
await inviteVerificationBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const invite = await req.db.getInviteToken(req.body.token);
res
.status(200)
.json({status: "success", msg: "Invite verified", data: invite});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined
? (error.method = "inviteVerifyController")
: null;
next(error);
}
try {
const invite = await req.db.getInviteToken(req.body.token);
res
.status(200)
.json({ status: "success", msg: "Invite verified", data: invite });
} catch (error) {
next(handleError(error, SERVICE_NAME, "inviteVerifyController"));
}
};
module.exports = {
inviteController: issueInvitation,
inviteVerifyController,
inviteController: issueInvitation,
inviteVerifyController,
};

View File

@@ -10,17 +10,14 @@ const {
const jwt = require("jsonwebtoken");
const { getTokenFromHeaders } = require("../utils/utils");
const { successMessages } = require("../utils/messages");
const { handleValidationError, handleError } = require("./controllerUtils");
const SERVICE_NAME = "maintenanceWindowController";
const createMaintenanceWindows = async (req, res, next) => {
try {
await createMaintenanceWindowBodyValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
@@ -45,11 +42,7 @@ const createMaintenanceWindows = async (req, res, next) => {
msg: successMessages.MAINTENANCE_WINDOW_CREATE,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined
? (error.method = "createMaintenanceWindow")
: null;
next(error);
next(handleError(error, SERVICE_NAME, "createMaintenanceWindow"));
}
};
@@ -57,11 +50,7 @@ const getMaintenanceWindowById = async (req, res, next) => {
try {
await getMaintenanceWindowByIdParamValidation.validateAsync(req.params);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
@@ -74,11 +63,7 @@ const getMaintenanceWindowById = async (req, res, next) => {
data: maintenanceWindow,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined
? (error.method = "getMaintenanceWindowById")
: null;
next(error);
next(handleError(error, SERVICE_NAME, "getMaintenanceWindowById"));
}
};
@@ -86,11 +71,7 @@ const getMaintenanceWindowsByTeamId = async (req, res, next) => {
try {
await getMaintenanceWindowsByTeamIdQueryValidation.validateAsync(req.query);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
@@ -109,11 +90,7 @@ const getMaintenanceWindowsByTeamId = async (req, res, next) => {
data: maintenanceWindows,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined
? (error.method = "getMaintenanceWindowsByUserId")
: null;
next(error);
next(handleError(error, SERVICE_NAME, "getMaintenanceWindowsByUserId"));
}
};
@@ -123,11 +100,7 @@ const getMaintenanceWindowsByMonitorId = async (req, res, next) => {
req.params
);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
@@ -142,11 +115,7 @@ const getMaintenanceWindowsByMonitorId = async (req, res, next) => {
data: maintenanceWindows,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined
? (error.method = "getMaintenanceWindowsByMonitorId")
: null;
next(error);
next(handleError(error, SERVICE_NAME, "getMaintenanceWindowsByMonitorId"));
}
};
@@ -154,11 +123,7 @@ const deleteMaintenanceWindow = async (req, res, next) => {
try {
await deleteMaintenanceWindowByIdParamValidation.validateAsync(req.params);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
@@ -168,10 +133,7 @@ const deleteMaintenanceWindow = async (req, res, next) => {
msg: successMessages.MAINTENANCE_WINDOW_DELETE,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined
? (error.method = "deleteMaintenanceWindow")
: null;
next(handleError(error, SERVICE_NAME, "deleteMaintenanceWindow"));
}
};
@@ -180,11 +142,7 @@ const editMaintenanceWindow = async (req, res, next) => {
await editMaintenanceWindowByIdParamValidation.validateAsync(req.params);
await editMaintenanceByIdWindowBodyValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
@@ -198,10 +156,7 @@ const editMaintenanceWindow = async (req, res, next) => {
data: editedMaintenanceWindow,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined
? (error.method = "editMaintenanceWindow")
: null;
next(handleError(error, SERVICE_NAME, "editMaintenanceWindow"));
}
};

View File

@@ -19,6 +19,7 @@ const { errorMessages, successMessages } = require("../utils/messages");
const jwt = require("jsonwebtoken");
const { getTokenFromHeaders } = require("../utils/utils");
const logger = require("../utils/logger");
const { handleError, handleValidationError } = require("./controllerUtils");
/**
* Returns all monitors
@@ -38,9 +39,7 @@ const getAllMonitors = async (req, res, next) => {
data: monitors,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "getAllMonitors") : null;
next(error);
next(handleError(error, SERVICE_NAME, "getAllMonitors"));
}
};
@@ -58,10 +57,7 @@ const getMonitorStatsById = async (req, res, next) => {
await getMonitorStatsByIdParamValidation.validateAsync(req.params);
await getMonitorStatsByIdQueryValidation.validateAsync(req.query);
} catch (error) {
error.status = 422;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
@@ -73,9 +69,7 @@ const getMonitorStatsById = async (req, res, next) => {
data: monitorStats,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "getMonitorStatsById") : null;
next(error);
next(handleError(error, SERVICE_NAME, "getMonitorStatsById"));
}
};
@@ -83,10 +77,7 @@ const getMonitorCertificate = async (req, res, next) => {
try {
await getCertificateParamValidation.validateAsync(req.params);
} catch (error) {
error.status = 422;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
}
try {
@@ -110,11 +101,7 @@ const getMonitorCertificate = async (req, res, next) => {
});
}
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined
? (error.method = "getMonitorCertificate")
: null;
next(error);
next(handleError(error, SERVICE_NAME, "getMonitorCertificate"));
}
};
@@ -134,10 +121,7 @@ const getMonitorById = async (req, res, next) => {
await getMonitorByIdParamValidation.validateAsync(req.params);
await getMonitorByIdQueryValidation.validateAsync(req.query);
} catch (error) {
error.status = 422;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
@@ -154,9 +138,7 @@ const getMonitorById = async (req, res, next) => {
data: monitor,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "getMonitorById") : null;
next(error);
next(handleError(error, SERVICE_NAME, "getMonitorById"));
}
};
@@ -179,14 +161,8 @@ const getMonitorsAndSummaryByTeamId = async (req, res, next) => {
req.params
);
await getMonitorsAndSummaryByTeamIdQueryValidation.validateAsync(req.query);
//validation
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.method === undefined? error.method = "getMonitorsAndSummaryByTeamId": null;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
@@ -203,11 +179,7 @@ const getMonitorsAndSummaryByTeamId = async (req, res, next) => {
data: monitorsSummary,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined
? (error.method = "getMonitorsAndSummaryByTeamId")
: null;
next(error);
next(handleError(error, SERVICE_NAME, "getMonitorsAndSummaryByTeamId"));
}
};
@@ -228,11 +200,7 @@ const getMonitorsByTeamId = async (req, res, next) => {
await getMonitorsByTeamIdValidation.validateAsync(req.params);
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
@@ -245,8 +213,7 @@ const getMonitorsByTeamId = async (req, res, next) => {
data: monitors,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "getMonitorsByTeamId") : null;
next(handleError(error, SERVICE_NAME, "getMonitorsByTeamId"));
next(error);
}
};
@@ -266,12 +233,7 @@ const createMonitor = async (req, res, next) => {
try {
await createMonitorBodyValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
@@ -281,10 +243,10 @@ const createMonitor = async (req, res, next) => {
if (notifications && notifications.length !== 0) {
monitor.notifications = await Promise.all(
notifications.map(async (notification) => {
notification.monitorId = monitor._id;
await req.db.createNotification(notification);
})
notifications.map(async (notification) => {
notification.monitorId = monitor._id;
await req.db.createNotification(notification);
})
);
await monitor.save();
}
@@ -296,9 +258,7 @@ const createMonitor = async (req, res, next) => {
data: monitor,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "createMonitor") : null;
next(error);
next(handleError(error, SERVICE_NAME, "createMonitor"));
}
};
@@ -317,11 +277,7 @@ const deleteMonitor = async (req, res, next) => {
try {
await getMonitorByIdParamValidation.validateAsync(req.params);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
@@ -347,9 +303,7 @@ const deleteMonitor = async (req, res, next) => {
.status(200)
.json({ success: true, msg: successMessages.MONITOR_DELETE });
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "deleteMonitor") : null;
next(error);
next(handleError(error, SERVICE_NAME, "deleteMonitor"));
}
};
@@ -382,9 +336,7 @@ const deleteAllMonitors = async (req, res, next) => {
.status(200)
.json({ success: true, msg: `Deleted ${deletedCount} monitors` });
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "deleteAllMonitors") : null;
next(error);
next(handleError(error, SERVICE_NAME, "deleteAllMonitors"));
}
};
@@ -406,11 +358,7 @@ const editMonitor = async (req, res, next) => {
await getMonitorByIdParamValidation.validateAsync(req.params);
await editMonitorBodyValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
@@ -444,9 +392,7 @@ const editMonitor = async (req, res, next) => {
data: editedMonitor,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "editMonitor") : null;
next(error);
next(handleError(error, SERVICE_NAME, "editMonitor"));
}
};
@@ -465,11 +411,7 @@ const pauseMonitor = async (req, res, next) => {
try {
await pauseMonitorParamValidation.validateAsync(req.params);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
}
try {
@@ -490,9 +432,7 @@ const pauseMonitor = async (req, res, next) => {
data: monitor,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "pauseMonitor") : null;
next(error);
next(handleError(error, SERVICE_NAME, "pauseMonitor"));
}
};
@@ -511,21 +451,19 @@ const addDemoMonitors = async (req, res, next) => {
try {
const token = getTokenFromHeaders(req.headers);
const { jwtSecret } = req.settingsService.getSettings();
const { _id, teamId } = jwt.verify(token, jwtSecret);
const demoMonitors = await req.db.addDemoMonitors(_id, teamId);
await demoMonitors.forEach(async (monitor) => {
await req.jobQueue.addJob(monitor._id, monitor);
});
await Promise.all(
demoMonitors.map((monitor) => req.jobQueue.addJob(monitor._id, monitor))
);
return res.status(200).json({
success: true,
message: successMessages.MONITOR_DEMO_ADDED,
data: demoMonitors.length,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "addDemoMonitors") : null;
next(error);
next(handleError(error, SERVICE_NAME, "addDemoMonitors"));
}
};

View File

@@ -1,3 +1,5 @@
const { handleError } = require("./controllerUtils");
const SERVICE_NAME = "JobQueueController";
const getMetrics = async (req, res, next) => {
@@ -7,9 +9,7 @@ const getMetrics = async (req, res, next) => {
.status(200)
.json({ success: true, msg: "Metrics retrieved", data: metrics });
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "getMetrics") : null;
next(error);
next(handleError(error, SERVICE_NAME, "getMetrics"));
return;
}
};
@@ -19,9 +19,7 @@ const getJobs = async (req, res, next) => {
const jobs = await req.jobQueue.getJobStats();
return res.status(200).json({ jobs });
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "getJobs") : null;
next(error);
next(handleError(error, SERVICE_NAME, "getJobs"));
return;
}
};
@@ -31,21 +29,17 @@ const addJob = async (req, res, next) => {
await req.jobQueue.addJob(Math.random().toString(36).substring(7));
return res.send("Added job");
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "addJob") : null;
next(error);
next(handleError(error, SERVICE_NAME, "addJob"));
return;
}
};
const obliterateQueue = async (req, res, next) => {
try {
const obliterated = await req.jobQueue.obliterate();
await req.jobQueue.obliterate();
return res.status(200).send("Obliterated queue");
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "obliterateQueue") : null;
next(error);
next(handleError(error, SERVICE_NAME, "obliterateQueue"));
return;
}
};

View File

@@ -1,6 +1,7 @@
const { successMessages } = require("../utils/messages");
const SERVICE_NAME = "SettingsController";
const { updateAppSettingsBodyValidation } = require("../validation/joi");
const { handleValidationError, handleError } = require("./controllerUtils");
const getAppSettings = async (req, res, next) => {
try {
@@ -12,9 +13,7 @@ const getAppSettings = async (req, res, next) => {
data: settings,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "getAppSettings") : null;
next(error);
next(handleError(error, SERVICE_NAME, "getAppSettings"));
}
};
@@ -22,11 +21,7 @@ const updateAppSettings = async (req, res, next) => {
try {
await updateAppSettingsBodyValidation.validateAsync(req.body);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
next(handleValidationError(error, SERVICE_NAME));
return;
}
@@ -40,9 +35,7 @@ const updateAppSettings = async (req, res, next) => {
data: updatedSettings,
});
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "updateAppSettings") : null;
next(error);
next(handleError(error, SERVICE_NAME, "updateAppSettings"));
}
};

View File

@@ -1,10 +1,7 @@
const jwt = require("jsonwebtoken");
const logger = require("../utils/logger");
const SERVICE_NAME = "verifyJWT";
const TOKEN_PREFIX = "Bearer ";
const { errorMessages } = require("../utils/messages");
const { parse } = require("path");
const User = require("../db/models/User");
/**
* Verifies the JWT token
* @function
@@ -38,16 +35,14 @@ const verifyJWT = (req, res, next) => {
const { jwtSecret } = req.settingsService.getSettings();
jwt.verify(parsedToken, jwtSecret, (err, decoded) => {
if (err) {
if (err.name === "TokenExpiredError") {
res
.status(401)
.json({ success: false, msg: errorMessages.EXPIRED_AUTH_TOKEN });
}
return res
.status(401)
.json({ success: false, msg: errorMessages.INVALID_AUTH_TOKEN });
const errorMessage =
err.name === "TokenExpiredError"
? errorMessages.EXPIRED_AUTH_TOKEN
: errorMessages.INVALID_AUTH_TOKEN;
return res.status(401).json({ success: false, msg: errorMessage });
}
//Add the user to the request object for use in the route
// Add the user to the request object for use in the route
req.user = decoded;
next();
});

2225
Server/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"test": "nyc mocha",
"dev": "nodemon index.js"
},
"keywords": [],
@@ -15,6 +15,7 @@
"axios": "^1.7.2",
"bcrypt": "^5.1.1",
"bullmq": "5.7.15",
"chai": "5.1.1",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
@@ -24,6 +25,7 @@
"jsonwebtoken": "9.0.2",
"mailersend": "^2.2.0",
"mjml": "^5.0.0-alpha.4",
"mocha": "10.7.3",
"mongoose": "^8.3.3",
"multer": "1.4.5-lts.1",
"nodemailer": "^6.9.14",
@@ -34,6 +36,8 @@
"winston": "^3.13.0"
},
"devDependencies": {
"nodemon": "3.1.0"
"nodemon": "3.1.0",
"nyc": "17.1.0",
"sinon": "19.0.2"
}
}

View File

@@ -0,0 +1,609 @@
const {
registerUser,
loginUser,
editUser,
checkSuperadminExists,
requestRecovery,
validateRecovery,
resetPassword,
deleteUser,
getAllUsers,
} = require("../../controllers/authController");
const jwt = require("jsonwebtoken");
const { errorMessages, successMessages } = require("../../utils/messages");
const sinon = require("sinon");
describe("Auth Controller - registerUser", () => {
// Set up test
beforeEach(() => {
req = {
db: {
checkSuperadmin: sinon.stub(),
getInviteTokenAndDelete: sinon.stub(),
updateAppSettings: sinon.stub(),
insertUser: sinon.stub(),
},
settingsService: {
getSettings: sinon.stub().resolves({
jwtSecret: "my_secret",
}),
},
emailService: {
buildAndSendEmail: sinon.stub().returns(Promise.resolve()),
},
file: {},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
});
it("should register a valid user", async () => {
req.body = {
firstName: "John",
lastName: "Doe",
email: "john.doe@example.com",
password: "Uptime1!",
inviteToken: "someToken",
role: ["user"],
teamId: "123",
};
req.db.checkSuperadmin.resolves(false);
req.db.insertUser.resolves({
_id: "123",
_doc: {
firstName: "John",
lastName: "Doe",
email: "john.doe@example.com",
},
});
await registerUser(req, res, next);
expect(res.status.calledWith(200)).to.be.true;
expect(
res.json.calledWith(
sinon.match({
success: true,
msg: sinon.match.string,
data: {
user: sinon.match.object,
token: sinon.match.string,
},
})
)
).to.be.true;
expect(next.notCalled).to.be.true;
});
it("should reject a user with an invalid password", async () => {
req.body = {
firstName: "John",
lastName: "Doe",
email: "john.doe@example.com",
password: "bad_password",
inviteToken: "someToken",
role: ["user"],
teamId: "123",
};
await registerUser(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should reject a user with an invalid role", async () => {
req.body = {
firstName: "John",
lastName: "Doe",
email: "john.doe@example.com",
password: "Uptime1!",
inviteToken: "someToken",
role: ["superman"],
teamId: "123",
};
await registerUser(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
});
describe("Auth Controller - loginUser", () => {
beforeEach(() => {
req = {
body: { email: "test@example.com", password: "Password123!" },
db: {
getUserByEmail: sinon.stub(),
},
settingsService: {
getSettings: sinon.stub().resolves({
jwtSecret: "my_secret",
}),
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
user = {
_doc: {
email: "test@example.com",
},
comparePassword: sinon.stub(),
};
});
it("should login user successfully", async () => {
req.db.getUserByEmail.resolves(user);
user.comparePassword.resolves(true);
await loginUser(req, res, next);
expect(res.status.calledWith(200)).to.be.true;
expect(
res.json.calledWith({
success: true,
msg: successMessages.AUTH_LOGIN_USER,
data: {
user: {
email: "test@example.com",
avatarImage: undefined,
},
token: sinon.match.string,
},
})
).to.be.true;
expect(next.notCalled).to.be.true;
});
it("should reject a user with an incorrect password", async () => {
req.body = {
email: "test@test.com",
password: "Password123!",
};
req.db.getUserByEmail.resolves(user);
user.comparePassword.resolves(false);
await loginUser(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal(
errorMessages.AUTH_INCORRECT_PASSWORD
);
});
});
describe("Auth Controller - editUser", async () => {
beforeEach(() => {
req = {
params: { userId: "123" },
body: { password: "Password1!", newPassword: "Password2!" },
headers: { authorization: "Bearer token" },
user: { _id: "123" },
settingsService: {
getSettings: sinon.stub().returns({ jwtSecret: "my_secret" }),
},
db: {
getUserByEmail: sinon.stub(),
updateUser: sinon.stub(),
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
});
it("should edit a user if it receives a proper request", async () => {
sinon.stub(jwt, "verify").returns({ email: "test@example.com" });
const user = {
comparePassword: sinon.stub().resolves(true),
};
req.db.getUserByEmail.resolves(user);
req.db.updateUser.resolves({ email: "test@example.com" });
await editUser(req, res, next);
expect(req.db.getUserByEmail.calledOnce).to.be.true;
expect(req.db.updateUser.calledOnce).to.be.true;
expect(res.status.calledWith(200)).to.be.true;
expect(
res.json.calledWith({
success: true,
msg: successMessages.AUTH_UPDATE_USER,
data: { email: "test@example.com" },
})
).to.be.true;
expect(next.notCalled).to.be.true;
});
it("should reject an edit request if password format is incorrect", async () => {
req.body = { password: "bad_password", newPassword: "bad_password" };
const user = {
comparePassword: sinon.stub().resolves(true),
};
req.db.getUserByEmail.resolves(user);
await editUser(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
});
describe("Auth Controller - checkSuperadminExists", async () => {
beforeEach(() => {
req = {
db: {
checkSuperadmin: sinon.stub(),
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
});
it("should return true if a superadmin exists", async () => {
req.db.checkSuperadmin.resolves(true);
await checkSuperadminExists(req, res, next);
expect(res.status.calledWith(200)).to.be.true;
expect(
res.json.calledWith({
success: true,
msg: successMessages.AUTH_SUPERADMIN_EXISTS,
data: true,
})
).to.be.true;
expect(next.notCalled).to.be.true;
});
it("should return false if a superadmin does not exist", async () => {
req.db.checkSuperadmin.resolves(false);
await checkSuperadminExists(req, res, next);
expect(res.status.calledWith(200)).to.be.true;
expect(
res.json.calledWith({
success: true,
msg: successMessages.AUTH_SUPERADMIN_EXISTS,
data: false,
})
).to.be.true;
expect(next.notCalled).to.be.true;
});
});
describe("Auth Controller - requestRecovery", async () => {
beforeEach(() => {
req = {
body: { email: "test@test.com" },
db: {
getUserByEmail: sinon.stub(),
requestRecoveryToken: sinon.stub(),
},
settingsService: {
getSettings: sinon.stub().returns({ clientHost: "http://localhost" }),
},
emailService: {
buildAndSendEmail: sinon.stub(),
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
});
it("should throw an error if the email is not provided", async () => {
req.body = {};
await requestRecovery(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should return a success message if the email is provided", async () => {
const user = { firstName: "John" };
const recoveryToken = { token: "recovery-token" };
const msgId = "message-id";
req.db.getUserByEmail.resolves(user);
req.db.requestRecoveryToken.resolves(recoveryToken);
req.emailService.buildAndSendEmail.resolves(msgId);
await requestRecovery(req, res, next);
expect(req.db.getUserByEmail.calledOnceWith("test@test.com")).to.be.true;
expect(req.db.requestRecoveryToken.calledOnceWith(req, res)).to.be.true;
expect(
req.emailService.buildAndSendEmail.calledOnceWith(
"passwordResetTemplate",
{
name: "John",
email: "test@test.com",
url: "http://localhost/set-new-password/recovery-token",
},
"test@test.com",
"Bluewave Uptime Password Reset"
)
).to.be.true;
expect(res.status.calledOnceWith(200)).to.be.true;
expect(
res.json.calledOnceWith({
success: true,
msg: successMessages.AUTH_CREATE_RECOVERY_TOKEN,
data: msgId,
})
).to.be.true;
expect(next.notCalled).to.be.true;
});
});
describe("Auth Controller - validateRecovery", async () => {
beforeEach(() => {
req = {
body: { recoveryToken: "recovery-token" },
db: {
validateRecoveryToken: sinon.stub(),
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
});
it("should call next with a validation error if the token is invalid", async () => {
req = {
body: {},
};
await validateRecovery(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should return a success message if the token is valid", async () => {
req.db.validateRecoveryToken.resolves();
await validateRecovery(req, res, next);
expect(res.status.calledOnceWith(200)).to.be.true;
expect(
res.json.calledOnceWith({
success: true,
msg: successMessages.AUTH_VERIFY_RECOVERY_TOKEN,
})
).to.be.true;
expect(next.notCalled).to.be.true;
});
});
describe("Auth Controller - resetPassword", async () => {
beforeEach(() => {
req = {
body: {
recoveryToken: "recovery-token",
password: "Password1!",
},
db: {
resetPassword: sinon.stub(),
},
settingsService: {
getSettings: sinon.stub(),
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
newPasswordValidation = {
validateAsync: sinon.stub(),
};
handleValidationError = sinon.stub();
handleError = sinon.stub();
issueToken = sinon.stub();
});
it("should call next with a validation error if the password is invalid", async () => {
req.body = { password: "bad_password" };
await resetPassword(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should reset password successfully", async () => {
const user = { _doc: {} };
const appSettings = { jwtSecret: "my_secret" };
const token = "token";
newPasswordValidation.validateAsync.resolves();
req.db.resetPassword.resolves(user);
req.settingsService.getSettings.resolves(appSettings);
issueToken.returns(token);
await resetPassword(req, res, next);
expect(req.db.resetPassword.calledOnceWith(req, res)).to.be.true;
expect(req.settingsService.getSettings.calledOnce).to.be.true;
expect(res.status.calledOnceWith(200)).to.be.true;
expect(
res.json.calledOnceWith({
success: true,
msg: successMessages.AUTH_RESET_PASSWORD,
data: { user: sinon.match.object, token: sinon.match.string },
})
).to.be.true;
expect(next.notCalled).to.be.true;
});
});
describe("Auth Controller - deleteUser", async () => {
beforeEach(() => {
req = {
headers: {
authorization: "Bearer token",
},
db: {
getUserByEmail: sinon.stub(),
getMonitorsByTeamId: sinon.stub(),
deleteJob: sinon.stub(),
deleteChecks: sinon.stub(),
deletePageSpeedChecksByMonitorId: sinon.stub(),
deleteNotificationsByMonitorId: sinon.stub(),
deleteTeam: sinon.stub(),
deleteAllOtherUsers: sinon.stub(),
deleteMonitorsByUserId: sinon.stub(),
deleteUser: sinon.stub(),
},
jobQueue: {
deleteJob: sinon.stub(),
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
sinon.stub(jwt, "decode");
handleError = sinon.stub();
});
afterEach(() => {
sinon.restore();
});
it("should return 404 if user is not found", async () => {
jwt.decode.returns({ email: "test@example.com" });
req.db.getUserByEmail.resolves(null);
await deleteUser(req, res, next);
expect(req.db.getUserByEmail.calledOnceWith("test@example.com")).to.be.true;
expect(next.calledOnce).to.be.true;
expect(next.firstCall.args[0].message).to.equal(
errorMessages.DB_USER_NOT_FOUND
);
expect(res.status.notCalled).to.be.true;
expect(res.json.notCalled).to.be.true;
});
it("should delete user and associated data if user is superadmin", async () => {
const user = {
_id: "user_id",
email: "test@example.com",
role: ["superadmin"],
teamId: "team_id",
};
const monitors = [{ _id: "monitor_id" }];
jwt.decode.returns({ email: "test@example.com" });
req.db.getUserByEmail.resolves(user);
req.db.getMonitorsByTeamId.resolves({ monitors });
await deleteUser(req, res, next);
expect(req.db.getUserByEmail.calledOnceWith("test@example.com")).to.be.true;
expect(
req.db.getMonitorsByTeamId.calledOnceWith({
params: { teamId: "team_id" },
})
).to.be.true;
expect(req.jobQueue.deleteJob.calledOnceWith(monitors[0])).to.be.true;
expect(req.db.deleteChecks.calledOnceWith("monitor_id")).to.be.true;
expect(req.db.deletePageSpeedChecksByMonitorId.calledOnceWith("monitor_id"))
.to.be.true;
expect(req.db.deleteNotificationsByMonitorId.calledOnceWith("monitor_id"))
.to.be.true;
expect(req.db.deleteTeam.calledOnceWith("team_id")).to.be.true;
expect(req.db.deleteAllOtherUsers.calledOnce).to.be.true;
expect(req.db.deleteMonitorsByUserId.calledOnceWith("user_id")).to.be.true;
expect(req.db.deleteUser.calledOnceWith("user_id")).to.be.true;
expect(res.status.calledOnceWith(200)).to.be.true;
expect(
res.json.calledOnceWith({
success: true,
msg: successMessages.AUTH_DELETE_USER,
})
).to.be.true;
expect(next.notCalled).to.be.true;
});
it("should delete user if user is not superadmin", async () => {
const user = {
_id: "user_id",
email: "test@example.com",
role: ["user"],
teamId: "team_id",
};
jwt.decode.returns({ email: "test@example.com" });
req.db.getUserByEmail.resolves(user);
await deleteUser(req, res, next);
expect(req.db.getUserByEmail.calledOnceWith("test@example.com")).to.be.true;
expect(
req.db.getMonitorsByTeamId.calledOnceWith({
params: { teamId: "team_id" },
})
).to.be.true;
expect(req.db.deleteUser.calledOnceWith("user_id")).to.be.true;
expect(res.status.calledOnceWith(200)).to.be.true;
expect(
res.json.calledOnceWith({
success: true,
msg: successMessages.AUTH_DELETE_USER,
})
).to.be.true;
expect(next.notCalled).to.be.true;
});
it("should handle errors", async () => {
const error = new Error("Something went wrong");
const SERVICE_NAME = "AuthController";
jwt.decode.returns({ email: "test@example.com" });
req.db.getUserByEmail.rejects(error);
await deleteUser(req, res, next);
expect(next.calledOnce).to.be.true;
expect(next.firstCall.args[0].message).to.equal("Something went wrong");
expect(res.status.notCalled).to.be.true;
expect(res.json.notCalled).to.be.true;
});
});
describe("Auth Controller - getAllUsers", async () => {
beforeEach(() => {
req = {
db: {
getAllUsers: sinon.stub(),
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
handleError = sinon.stub();
});
afterEach(() => {
sinon.restore(); // Restore the original methods after each test
});
it("should return 200 and all users", async () => {
const allUsers = [{ id: 1, name: "John Doe" }];
req.db.getAllUsers.resolves(allUsers);
await getAllUsers(req, res, next);
expect(req.db.getAllUsers.calledOnce).to.be.true;
expect(res.status.calledOnceWith(200)).to.be.true;
expect(
res.json.calledOnceWith({
success: true,
msg: "Got all users",
data: allUsers,
})
).to.be.true;
expect(next.notCalled).to.be.true;
});
it("should call next with error when an exception occurs", async () => {
const error = new Error("Something went wrong");
req.db.getAllUsers.rejects(error);
await getAllUsers(req, res, next);
expect(req.db.getAllUsers.calledOnce).to.be.true;
expect(next.calledOnce).to.be.true;
expect(res.status.notCalled).to.be.true;
expect(res.json.notCalled).to.be.true;
});
});