Merge branch 'develop' into feat/fe/statuspage-3

This commit is contained in:
Shemy Gan committed 2024-12-30 13:06:22 -05:00
commit d8da182f60
48 files changed
+2777 -2333

No files matched your search

+543 -379
View File
File diff suppressed because it is too large. Load diff
+4 -4
View File
@@ -14,11 +14,11 @@
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@fontsource/roboto": "^5.0.13",
"@mui/icons-material": "6.2.1",
"@mui/lab": "6.0.0-beta.20",
"@mui/material": "6.2.1",
"@mui/icons-material": "6.3.0",
"@mui/lab": "6.0.0-beta.21",
"@mui/material": "6.3.0",
"@mui/x-charts": "^7.5.1",
"@mui/x-data-grid": "7.23.3",
"@mui/x-data-grid": "7.23.5",
"@mui/x-date-pickers": "7.23.3",
"@reduxjs/toolkit": "2.5.0",
"axios": "^1.7.4",
+1 -1
View File
@@ -100,7 +100,7 @@ const PATH_MAP = {
pagespeed: "Dashboard",
infrastructure: "Dashboard",
account: "Account",
settings: "Other",
settings: "Settings",
};
/**
@@ -86,7 +86,7 @@ export const getUptimeMonitorsByTeamId = createAsyncThunk(
const res = await networkService.getMonitorsAndSummaryByTeamId({
authToken: token,
teamId: user.teamId,
types: ["http", "ping", "docker"],
types: ["http", "ping", "docker", "port"],
});
return res.data;
} catch (error) {
@@ -50,6 +50,11 @@ const CreateMonitor = () => {
placeholder: "abc123",
namePlaceholder: "My Container",
},
port: {
label: "URL to monitor",
placeholder: "localhost",
namePlaceholder: "Localhost:5173",
},
};
const { user, authToken } = useSelector((state) => state.auth);
@@ -82,6 +87,7 @@ const CreateMonitor = () => {
monitor.type === "http"
? `http${https ? "s" : ""}://` + monitor.url
: monitor.url,
port: monitor.type === "port" ? monitor.port : undefined,
name: monitor.name === "" ? monitor.url : monitor.name,
type: monitor.type,
interval: monitor.interval * MS_PER_MINUTE,
@@ -252,11 +258,23 @@ const CreateMonitor = () => {
error={errors["url"] ? true : false}
helperText={errors["url"]}
/>
<TextInput
type="number"
id="monitor-port"
label="Port to monitor"
placeholder="5173"
value={monitor.port}
onChange={(event) => handleChange(event, "port")}
error={errors["port"] ? true : false}
helperText={errors["port"]}
hidden={monitor.type !== "port"}
/>
<TextInput
type="text"
id="monitor-name"
label="Display name"
isOptional={true}
placeholder={monitorTypeMaps[monitor.type].namePlaceholder || ""}
value={monitor.name}
onChange={(event) => handleChange(event, "name")}
error={errors["name"] ? true : false}
@@ -321,6 +339,15 @@ const CreateMonitor = () => {
checked={monitor.type === "docker"}
onChange={(event) => handleChange(event, "type")}
/>
<Radio
id="monitor-checks-port"
title="Port monitoring"
desc="Check whether your port is open or not."
size="small"
value="port"
checked={monitor.type === "port"}
onChange={(event) => handleChange(event, "type")}
/>
{errors["type"] ? (
<Box className="error-container">
<Typography
+18 -69
View File
@@ -1,14 +1,6 @@
import PropTypes from "prop-types";
import { useEffect, useState, useCallback } from "react";
import {
Box,
Button,
Popover,
Stack,
Tooltip,
Typography,
useTheme,
} from "@mui/material";
import { Box, Button, Stack, Tooltip, Typography, useTheme } from "@mui/material";
import { useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import { networkService } from "../../../main";
@@ -17,7 +9,6 @@ import { formatDurationRounded, formatDurationSplit } from "../../../Utils/timeU
import MonitorDetailsAreaChart from "../../../Components/Charts/MonitorDetailsAreaChart";
import ButtonGroup from "@mui/material/ButtonGroup";
import SettingsIcon from "../../../assets/icons/settings-bold.svg?react";
import CertificateIcon from "../../../assets/icons/certificate.svg?react";
import UptimeIcon from "../../../assets/icons/uptime-icon.svg?react";
import ResponseTimeIcon from "../../../assets/icons/response-time-icon.svg?react";
import AverageResponseIcon from "../../../assets/icons/average-response-icon.svg?react";
@@ -50,14 +41,7 @@ const DetailsPage = () => {
const [certificateExpiry, setCertificateExpiry] = useState("N/A");
const navigate = useNavigate();
const [anchorEl, setAnchorEl] = useState(null);
const openCertificate = (event) => {
setAnchorEl(event.currentTarget);
};
const closeCertificate = () => {
setAnchorEl(null);
};
const certificateDateFormat = "MMM D, YYYY h A";
const dateFormat = dateRange === "day" ? "MMM D, h A" : "MMM D";
const uiTimezone = useSelector((state) => state.ui.timezone);
@@ -72,6 +56,7 @@ const DetailsPage = () => {
numToDisplay: 50,
normalize: true,
});
console.log(res?.data?.data);
setMonitor(res?.data?.data ?? {});
} catch (error) {
logger.error(error);
@@ -95,7 +80,9 @@ const DetailsPage = () => {
});
if (res?.data?.data?.certificateDate) {
const date = res.data.data.certificateDate;
setCertificateExpiry(formatDateWithTz(date, dateFormat, uiTimezone) ?? "N/A");
setCertificateExpiry(
formatDateWithTz(date, certificateDateFormat, uiTimezone) ?? "N/A"
);
}
} catch (error) {
setCertificateExpiry("N/A");
@@ -210,56 +197,6 @@ const DetailsPage = () => {
alignSelf: "flex-end",
}}
>
<IconBox
mr={theme.spacing(4)}
onClick={openCertificate}
sx={{
cursor: "pointer",
"& svg": {
width: 23,
height: 23,
top: "52%",
},
}}
>
<CertificateIcon />
</IconBox>
<Popover
id="certificate-dropdown"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={closeCertificate}
disableScrollLock
marginThreshold={null}
anchorOrigin={{
vertical: "bottom",
horizontal: "center",
}}
transformOrigin={{
vertical: "top",
horizontal: "center",
}}
slotProps={{
paper: {
sx: {
mt: theme.spacing(4),
py: theme.spacing(2),
px: theme.spacing(4),
width: 140,
backgroundColor: theme.palette.background.accent,
},
},
}}
>
<Typography variant="body2">Certificate Expiry</Typography>
<Typography
component="span"
fontSize={13}
color={theme.palette.text.primary}
>
{certificateExpiry}
</Typography>
</Popover>
{isAdmin && (
<Button
variant="contained"
@@ -302,6 +239,18 @@ const DetailsPage = () => {
</>
}
/>
<StatBox
heading="certificate expiry"
subHeading={
<Typography
component="span"
fontSize={13}
color={theme.palette.text.primary}
>
{certificateExpiry}
</Typography>
}
/>
</Stack>
<Box>
<Stack
@@ -74,7 +74,7 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching, handlePaus
authToken,
teamId: user.teamId,
limit: 25,
types: ["http", "ping", "docker"],
types: ["http", "ping", "docker", "port"],
status: null,
checkOrder: "desc",
normalize: true,
+2 -2
View File
@@ -54,7 +54,7 @@ const paletteColors = {
red200: "#F04438",
red300: "#D32F2F",
red700: "#542426",
red800: "#301A1F",
red800: "#912018",
orange50: "#FEF8EA",
orange100: "#FFECBC",
orange300: "#FDB022",
@@ -128,7 +128,7 @@ const semanticColors = {
dark: paletteColors.red700,
},
dark: {
light: paletteColors.red50,
light: paletteColors.red800,
dark: paletteColors.red800,
},
},
+1
View File
@@ -109,6 +109,7 @@ const monitorValidation = joi.object({
"string.uri": "The URL you provided is not valid.",
"string.invalidUrl": "Please enter a valid URL with optional port",
}),
port: joi.number(),
name: joi.string().trim().max(50).allow("").messages({
"string.max": "This field should not exceed the 50 characters limit.",
}),
+4
View File
@@ -1,6 +1,7 @@
services:
client:
image: uptime_client:latest
restart: always
ports:
- "5173:5173"
@@ -8,6 +9,7 @@ services:
- server
server:
image: uptime_server:latest
restart: always
ports:
- "5000:5000"
env_file:
@@ -17,12 +19,14 @@ services:
- mongodb
redis:
image: uptime_redis:latest
restart: always
ports:
- "6379:6379"
volumes:
- ./redis/data:/data
mongodb:
image: uptime_database_mongo:latest
restart: always
command: ["mongod", "--quiet"]
ports:
- "27017:27017"
+4
View File
@@ -1,6 +1,7 @@
services:
client:
image: bluewaveuptime/uptime_client:latest
restart: always
environment:
UPTIME_APP_API_BASE_URL: "http://localhost:5000/api/v1"
ports:
@@ -10,6 +11,7 @@ services:
- server
server:
image: bluewaveuptime/uptime_server:latest
restart: always
ports:
- "5000:5000"
depends_on:
@@ -22,12 +24,14 @@ services:
# - /var/run/docker.sock:/var/run/docker.sock:ro
redis:
image: bluewaveuptime/uptime_redis:latest
restart: always
ports:
- "6379:6379"
volumes:
- ./redis/data:/data
mongodb:
image: bluewaveuptime/uptime_database_mongo:latest
restart: always
volumes:
- ./mongo/data:/data/db
command: ["mongod", "--quiet"]
+5
View File
@@ -1,6 +1,7 @@
services:
client:
image: uptime_client:latest
restart: always
environment:
UPTIME_APP_API_BASE_URL: "https://checkmate-demo.bluewavelabs.ca/api/v1"
ports:
@@ -15,11 +16,13 @@ services:
certbot:
image: certbot/certbot:latest
restart: always
volumes:
- ./certbot/www/:/var/www/certbot/:rw
- ./certbot/conf/:/etc/letsencrypt/:rw
server:
image: uptime_server:latest
restart: always
ports:
- "5000:5000"
env_file:
@@ -29,12 +32,14 @@ services:
- mongodb
redis:
image: uptime_redis:latest
restart: always
ports:
- "6379:6379"
volumes:
- ./redis/data:/data
mongodb:
image: uptime_database_mongo:latest
restart: always
command: ["mongod", "--quiet", "--auth"]
ports:
- "27017:27017"
+5 -1
View File
@@ -1,8 +1,9 @@
services:
client:
image: uptime_client:latest
restart: always
environment:
UPTIME_APP_API_BASE_URL: "https://checkmate-demo.bluewavelabs.ca/api/v1"
UPTIME_APP_API_BASE_URL: "https://checkmate-test.bluewavelabs.ca/api/v1"
ports:
- "80:80"
- "443:443"
@@ -15,11 +16,13 @@ services:
certbot:
image: certbot/certbot:latest
restart: always
volumes:
- ./certbot/www/:/var/www/certbot/:rw
- ./certbot/conf/:/etc/letsencrypt/:rw
server:
image: uptime_server:latest
restart: always
ports:
- "5000:5000"
env_file:
@@ -29,6 +32,7 @@ services:
- mongodb
redis:
image: uptime_redis:latest
restart: always
ports:
- "6379:6379"
volumes:
+4 -3
View File
@@ -98,15 +98,16 @@ Here's how you can contribute:
5. Check for good-first-issue's if you are a newcomer.
6. Make a pull request to add new features/make quality-of-life improvements/fix bugs.
<!-- <a href="https://github.com/bluewave-labs/checkmate/graphs/contributors">
<a href="https://github.com/bluewave-labs/checkmate/graphs/contributors">
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
</a>
Made with [contrib.rocks](https://contrib.rocks).
-->
<!--
![Alt](https://repobeats.axiom.co/api/embed/c35d999c82dbb31e967427ea4166c14da4172e73.svg "Repobeats analytics image")
-->
[![Star History Chart](https://api.star-history.com/svg?repos=bluewave-labs/checkmate&type=Date)](https://star-history.com/#bluewave-labs/bluewave-uptime&Date)
Also check other developer and contributor-friendly projects of BlueWave:
+471 -470
View File
@@ -17,500 +17,501 @@ import { handleValidationError, handleError } from "./controllerUtils.js";
const SERVICE_NAME = "authController";
dotenv.config();
/**
* Creates and returns JWT token with an arbitrary payload
* @function
* @param {Object} payload
* @param {tokenType} typeOfToken - Whether to generate refresh token with long TTL or access token with short TTL.
* @param {Object} appSettings
* @returns {String}
* @throws {Error}
*/
const issueToken = (payload, typeOfToken, appSettings) => {
try {
const tokenTTL =
typeOfToken === tokenType.REFRESH_TOKEN
? (appSettings?.refreshTokenTTL ?? "7d")
: (appSettings?.jwtTTL ?? "2h");
const tokenSecret =
typeOfToken === tokenType.REFRESH_TOKEN
? appSettings?.refreshTokenSecret
: appSettings?.jwtSecret;
const payloadData = typeOfToken === tokenType.REFRESH_TOKEN ? {} : payload;
return jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL });
} catch (error) {
throw handleError(error, SERVICE_NAME, "issueToken");
class AuthController {
constructor(db, settingsService, emailService, jobQueue) {
this.db = db;
this.settingsService = settingsService;
this.emailService = emailService;
this.jobQueue = jobQueue;
}
};
/**
* Registers a new user. If the user is the first account, a JWT secret is created. If not, an invite token is required.
* @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).
*/
const registerUser = async (req, res, next) => {
// 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 });
/**
* Creates and returns JWT token with an arbitrary payload
* @function
* @param {Object} payload
* @param {tokenType} typeOfToken - Whether to generate refresh token with long TTL or access token with short TTL.
* @param {Object} appSettings
* @returns {String}
* @throws {Error}
*/
issueToken = (payload, typeOfToken, appSettings) => {
try {
const tokenTTL =
typeOfToken === tokenType.REFRESH_TOKEN
? (appSettings?.refreshTokenTTL ?? "7d")
: (appSettings?.jwtTTL ?? "2h");
const tokenSecret =
typeOfToken === tokenType.REFRESH_TOKEN
? appSettings?.refreshTokenSecret
: appSettings?.jwtSecret;
const payloadData = typeOfToken === tokenType.REFRESH_TOKEN ? {} : payload;
return jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL });
} catch (error) {
throw handleError(error, SERVICE_NAME, "issueToken");
}
};
const newUser = await req.db.insertUser({ ...req.body }, req.file);
logger.info({
message: successMessages.AUTH_CREATE_USER,
service: SERVICE_NAME,
details: newUser._id,
});
/**
* Registers a new user. If the user is the first account, a JWT secret is created. If not, an invite token is required.
* @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).
*/
registerUser = async (req, res, next) => {
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 this.db.checkSuperadmin(req, res);
if (superAdminExists) {
await this.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 this.db.updateAppSettings({ jwtSecret });
}
const userForToken = { ...newUser._doc };
delete userForToken.profileImage;
delete userForToken.avatarImage;
const appSettings = await req.settingsService.getSettings();
const token = issueToken(userForToken, tokenType.ACCESS_TOKEN, appSettings);
const refreshToken = issueToken({}, tokenType.REFRESH_TOKEN, appSettings);
req.emailService
.buildAndSendEmail(
"welcomeEmailTemplate",
{ name: newUser.firstName },
newUser.email,
"Welcome to Uptime Monitor"
)
.catch((error) => {
logger.error({
message: error.message,
service: SERVICE_NAME,
method: "registerUser",
stack: error.stack,
});
const newUser = await this.db.insertUser({ ...req.body }, req.file);
logger.info({
message: successMessages.AUTH_CREATE_USER,
service: SERVICE_NAME,
details: newUser._id,
});
return res.status(200).json({
success: true,
msg: successMessages.AUTH_CREATE_USER,
data: { user: newUser, token: token, refreshToken: refreshToken },
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "registerController"));
}
};
const userForToken = { ...newUser._doc };
delete userForToken.profileImage;
delete userForToken.avatarImage;
/**
* Logs in a user by validating the user's credentials and issuing 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.
*/
const loginUser = async (req, res, next) => {
try {
await loginValidation.validateAsync(req.body);
} catch (error) {
const validationError = handleValidationError(error, SERVICE_NAME);
next(validationError);
return;
}
try {
const { email, password } = req.body;
const appSettings = await this.settingsService.getSettings();
// Check if user exists
const user = await req.db.getUserByEmail(email);
const token = this.issueToken(userForToken, tokenType.ACCESS_TOKEN, appSettings);
const refreshToken = this.issueToken({}, tokenType.REFRESH_TOKEN, appSettings);
// Compare password
const match = await user.comparePassword(password);
if (match !== true) {
next(new Error(errorMessages.AUTH_INCORRECT_PASSWORD));
this.emailService
.buildAndSendEmail(
"welcomeEmailTemplate",
{ name: newUser.firstName },
newUser.email,
"Welcome to Uptime Monitor"
)
.catch((error) => {
logger.error({
message: error.message,
service: SERVICE_NAME,
method: "registerUser",
stack: error.stack,
});
});
return res.status(200).json({
success: true,
msg: successMessages.AUTH_CREATE_USER,
data: { user: newUser, token: token, refreshToken: refreshToken },
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "registerController"));
}
};
/**
* Logs in a user by validating the user's credentials and issuing 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.
*/
loginUser = async (req, res, next) => {
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 this.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;
// Happy path, return token
const appSettings = await this.settingsService.getSettings();
const token = this.issueToken(
userWithoutPassword,
tokenType.ACCESS_TOKEN,
appSettings
);
const refreshToken = this.issueToken({}, tokenType.REFRESH_TOKEN, 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, refreshToken: refreshToken },
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "loginUser"));
}
};
/**
* Generates new auth token if the refresh token is valid
* @async
* @param {Express.Request} req - The Express request object.
* @property {Object} req.headers - The parameter of the request.
* @param {Express.Response} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message indicating new auth token is generated.
* @throws {Error} If there is an error during the process such as any of the token is not received
*/
refreshAuthToken = async (req, res, next) => {
try {
// check for refreshToken
const refreshToken = req.headers["x-refresh-token"];
if (!refreshToken) {
// No refresh token provided
const error = new Error(errorMessages.NO_REFRESH_TOKEN);
error.status = 401;
error.service = SERVICE_NAME;
error.method = "refreshAuthToken";
return next(error);
}
// Verify refresh token
const appSettings = await this.settingsService.getSettings();
const { refreshTokenSecret } = appSettings;
jwt.verify(refreshToken, refreshTokenSecret, async (refreshErr, refreshDecoded) => {
if (refreshErr) {
// Invalid or expired refresh token, trigger logout
const errorMessage =
refreshErr.name === "TokenExpiredError"
? errorMessages.EXPIRED_REFRESH_TOKEN
: errorMessages.INVALID_REFRESH_TOKEN;
const error = new Error(errorMessage);
error.status = 401;
error.service = SERVICE_NAME;
return next(error);
}
});
// Refresh token is valid and unexpired, generate new access token
const oldAuthToken = getTokenFromHeaders(req.headers);
const { jwtSecret } = await this.settingsService.getSettings();
const payloadData = jwt.verify(oldAuthToken, jwtSecret, { ignoreExpiration: true });
// delete old token related data
delete payloadData.iat;
delete payloadData.exp;
const newAuthToken = this.issueToken(
payloadData,
tokenType.ACCESS_TOKEN,
appSettings
);
return res.status(200).json({
success: true,
msg: successMessages.AUTH_TOKEN_REFRESHED,
data: { user: payloadData, token: newAuthToken, refreshToken: refreshToken },
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "refreshAuthToken"));
}
};
/**
* Edits a user's information. If the user wants to change their password, the current password is checked before updating to the new password.
* @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).
*/
editUser = async (req, res, next) => {
try {
await editUserParamValidation.validateAsync(req.params);
await editUserBodyValidation.validateAsync(req.body);
} catch (error) {
const validationError = handleValidationError(error, SERVICE_NAME);
next(validationError);
return;
}
// 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, tokenType.ACCESS_TOKEN, appSettings);
const refreshToken = issueToken({}, tokenType.REFRESH_TOKEN, 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, refreshToken: refreshToken },
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "loginUser"));
}
};
/**
* Generates new auth token if the refresh token is valid
* @async
* @param {Express.Request} req - The Express request object.
* @property {Object} req.headers - The parameter of the request.
* @param {Express.Response} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message indicating new auth token is generated.
* @throws {Error} If there is an error during the process such as any of the token is not received
*/
const refreshAuthToken = async (req, res, next) => {
try {
// check for refreshToken
const refreshToken = req.headers["x-refresh-token"];
if (!refreshToken) {
// No refresh token provided
const error = new Error(errorMessages.NO_REFRESH_TOKEN);
// 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;
error.method = "refreshAuthToken";
return next(error);
next(error);
return;
}
// Verify refresh token
const appSettings = await req.settingsService.getSettings();
const { refreshTokenSecret } = appSettings;
jwt.verify(refreshToken, refreshTokenSecret, async (refreshErr, refreshDecoded) => {
if (refreshErr) {
// Invalid or expired refresh token, trigger logout
const errorMessage =
refreshErr.name === "TokenExpiredError"
? errorMessages.EXPIRED_REFRESH_TOKEN
: errorMessages.INVALID_REFRESH_TOKEN;
const error = new Error(errorMessage);
error.status = 401;
error.service = SERVICE_NAME;
return next(error);
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 } = 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) {
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;
}
});
// Refresh token is valid and unexpired, generate new access token
const oldAuthToken = getTokenFromHeaders(req.headers);
const { jwtSecret } = await req.settingsService.getSettings();
const payloadData = jwt.verify(oldAuthToken, jwtSecret, { ignoreExpiration: true });
// delete old token related data
delete payloadData.iat;
delete payloadData.exp;
const newAuthToken = issueToken(payloadData, tokenType.ACCESS_TOKEN, appSettings);
return res.status(200).json({
success: true,
msg: successMessages.AUTH_TOKEN_REFRESHED,
data: { user: payloadData, token: newAuthToken, refreshToken: refreshToken },
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "refreshAuthToken"));
}
};
const updatedUser = await this.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"));
}
};
/**
* Edits a user's information. If the user wants to change their password, the current password is checked before updating to the new password.
* @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).
*/
const editUser = async (req, res, next) => {
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
// 403 instead of 401 to avoid triggering axios interceptor
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;
/**
* Checks if a superadmin account exists in the database.
* @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.
*/
checkSuperadminExists = async (req, res, next) => {
try {
const superAdminExists = await this.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"));
}
};
/**
* Requests a recovery token for a user. The user's email is validated and a recovery token is created and sent via 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).
*/
requestRecovery = async (req, res, next) => {
try {
await recoveryValidation.validateAsync(req.body);
} catch (error) {
const validationError = handleValidationError(error, SERVICE_NAME);
next(validationError);
return;
}
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"));
}
};
/**
* Checks if a superadmin account exists in the database.
* @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.
*/
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) {
next(handleError(error, SERVICE_NAME, "checkSuperadminController"));
}
};
/**
* Requests a recovery token for a user. The user's email is validated and a recovery token is created and sent via 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).
*/
const requestRecovery = async (req, res, next) => {
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);
const recoveryToken = await req.db.requestRecoveryToken(req, res);
const name = user.firstName;
const { clientHost } = req.settingsService.getSettings();
const url = `${clientHost}/set-new-password/${recoveryToken.token}`;
const msgId = await req.emailService.buildAndSendEmail(
"passwordResetTemplate",
{
name,
try {
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 msgId = await this.emailService.buildAndSendEmail(
"passwordResetTemplate",
{
name,
email,
url,
},
email,
url,
},
email,
"Checkmate Password Reset"
);
"Checkmate 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"));
}
};
/**
* Validates a recovery token. The recovery token is validated and if valid, a success message is returned.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.token - The recovery token to be validated.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status and a message indicating the validation of the recovery token.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
const validateRecovery = async (req, res, next) => {
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) {
next(handleError(error, SERVICE_NAME, "validateRecoveryTokenController"));
}
};
/**
* Resets a user's password. The new password is validated and if valid, the user's password is updated in the database and a new JWT token is issued.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.token - The recovery token.
* @property {string} req.body.password - The new password of the user.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message indicating the reset of the password, the updated user data (without password and avatar image), and a new JWT token.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
const resetPassword = async (req, res, next) => {
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, tokenType.ACCESS_TOKEN, appSettings);
res.status(200).json({
success: true,
msg: successMessages.AUTH_RESET_PASSWORD,
data: { user, token },
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "resetPasswordController"));
}
};
/**
* Deletes a user and all associated monitors, checks, and alerts.
*
* @param {Object} req - The request object.
* @param {Object} res - The response object.
* @param {Function} next - The next middleware function.
* @returns {Object} The response object with success status and message.
* @throws {Error} If user validation fails or user is not found in the database.
*/
const deleteUser = async (req, res, next) => {
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);
// 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);
return res.status(200).json({
success: true,
msg: successMessages.AUTH_CREATE_RECOVERY_TOKEN,
data: msgId,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "recoveryRequestController"));
}
};
/**
* Validates a recovery token. The recovery token is validated and if valid, a success message is returned.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.token - The recovery token to be validated.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status and a message indicating the validation of the recovery token.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
validateRecovery = async (req, res, next) => {
try {
await recoveryTokenValidation.validateAsync(req.body);
} catch (error) {
const validationError = handleValidationError(error, SERVICE_NAME);
next(validationError);
return;
}
// 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, next) => {
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"));
}
};
try {
await this.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"));
}
};
export {
issueToken,
registerUser,
loginUser,
refreshAuthToken,
editUser,
checkSuperadminExists,
requestRecovery,
validateRecovery,
resetPassword,
deleteUser,
getAllUsers,
};
/**
* Resets a user's password. The new password is validated and if valid, the user's password is updated in the database and a new JWT token is issued.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.token - The recovery token.
* @property {string} req.body.password - The new password of the user.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message indicating the reset of the password, the updated user data (without password and avatar image), and a new JWT token.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
resetPassword = async (req, res, next) => {
try {
await newPasswordValidation.validateAsync(req.body);
} catch (error) {
const validationError = handleValidationError(error, SERVICE_NAME);
next(validationError);
return;
}
try {
const user = await this.db.resetPassword(req, res);
const appSettings = await this.settingsService.getSettings();
const token = this.issueToken(user._doc, tokenType.ACCESS_TOKEN, appSettings);
res.status(200).json({
success: true,
msg: successMessages.AUTH_RESET_PASSWORD,
data: { user, token },
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "resetPasswordController"));
}
};
/**
* Deletes a user and all associated monitors, checks, and alerts.
*
* @param {Object} req - The request object.
* @param {Object} res - The response object.
* @param {Function} next - The next middleware function.
* @returns {Object} The response object with success status and message.
* @throws {Error} If user validation fails or user is not found in the database.
*/
deleteUser = async (req, res, next) => {
try {
const token = getTokenFromHeaders(req.headers);
const decodedToken = jwt.decode(token);
const { email } = decodedToken;
// Check if the user exists
const user = await this.db.getUserByEmail(email);
// 1. Find all the monitors associated with the team ID if superadmin
const result = await this.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 this.jobQueue.deleteJob(monitor);
await this.db.deleteChecks(monitor._id);
await this.db.deletePageSpeedChecksByMonitorId(monitor._id);
await this.db.deleteNotificationsByMonitorId(monitor._id);
})
));
// 3. Delete team
await this.db.deleteTeam(user.teamId);
// 4. Delete all other team members
await this.db.deleteAllOtherUsers();
// 5. Delete each monitor
await this.db.deleteMonitorsByUserId(user._id);
}
// 6. Delete the user by id
await this.db.deleteUser(user._id);
return res.status(200).json({
success: true,
msg: successMessages.AUTH_DELETE_USER,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "deleteUserController"));
}
};
getAllUsers = async (req, res, next) => {
try {
const allUsers = await this.db.getAllUsers(req, res);
res.status(200).json({ success: true, msg: "Got all users", data: allUsers });
} catch (error) {
next(handleError(error, SERVICE_NAME, "getAllUsersController"));
}
};
}
export default AuthController;
+122 -123
View File
@@ -16,139 +16,138 @@ import { handleValidationError, handleError } from "./controllerUtils.js";
const SERVICE_NAME = "checkController";
const createCheck = async (req, res, next) => {
try {
await createCheckParamValidation.validateAsync(req.params);
await createCheckBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
class CheckController {
constructor(db, settingsService) {
this.db = db;
this.settingsService = settingsService;
}
try {
const checkData = { ...req.body };
const check = await req.db.createCheck(checkData);
return res
.status(200)
.json({ success: true, msg: successMessages.CHECK_CREATE, data: check });
} catch (error) {
next(handleError(error, SERVICE_NAME, "createCheck"));
}
};
createCheck = async (req, res, next) => {
try {
await createCheckParamValidation.validateAsync(req.params);
await createCheckBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
const getChecks = async (req, res, next) => {
try {
await getChecksParamValidation.validateAsync(req.params);
await getChecksQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const checkData = { ...req.body };
const check = await this.db.createCheck(checkData);
return res
.status(200)
.json({ success: true, msg: successMessages.CHECK_CREATE, data: check });
} catch (error) {
next(handleError(error, SERVICE_NAME, "createCheck"));
}
};
try {
const checks = await req.db.getChecks(req);
const checksCount = await req.db.getChecksCount(req);
return res.status(200).json({
success: true,
msg: successMessages.CHECK_GET,
data: { checksCount, checks },
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getChecks"));
}
};
getChecks = async (req, res, next) => {
try {
await getChecksParamValidation.validateAsync(req.params);
await getChecksQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
const getTeamChecks = async (req, res, next) => {
try {
await getTeamChecksParamValidation.validateAsync(req.params);
await getTeamChecksQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const checkData = await req.db.getTeamChecks(req);
return res.status(200).json({
success: true,
msg: successMessages.CHECK_GET,
data: checkData,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getTeamChecks"));
}
};
try {
const checks = await this.db.getChecks(req);
const checksCount = await this.db.getChecksCount(req);
return res.status(200).json({
success: true,
msg: successMessages.CHECK_GET,
data: { checksCount, checks },
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getChecks"));
}
};
const deleteChecks = async (req, res, next) => {
try {
await deleteChecksParamValidation.validateAsync(req.params);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
getTeamChecks = async (req, res, next) => {
try {
await getTeamChecksParamValidation.validateAsync(req.params);
await getTeamChecksQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const checkData = await this.db.getTeamChecks(req);
return res.status(200).json({
success: true,
msg: successMessages.CHECK_GET,
data: checkData,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getTeamChecks"));
}
};
try {
const deletedCount = await req.db.deleteChecks(req.params.monitorId);
return res.status(200).json({
success: true,
msg: successMessages.CHECK_DELETE,
data: { deletedCount },
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "deleteChecks"));
}
};
deleteChecks = async (req, res, next) => {
try {
await deleteChecksParamValidation.validateAsync(req.params);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
const deleteChecksByTeamId = async (req, res, next) => {
try {
await deleteChecksByTeamIdParamValidation.validateAsync(req.params);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const deletedCount = await this.db.deleteChecks(req.params.monitorId);
return res.status(200).json({
success: true,
msg: successMessages.CHECK_DELETE,
data: { deletedCount },
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "deleteChecks"));
}
};
try {
const deletedCount = await req.db.deleteChecksByTeamId(req.params.teamId);
return res.status(200).json({
success: true,
msg: successMessages.CHECK_DELETE,
data: { deletedCount },
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "deleteChecksByTeamId"));
}
};
deleteChecksByTeamId = async (req, res, next) => {
try {
await deleteChecksByTeamIdParamValidation.validateAsync(req.params);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
const updateChecksTTL = async (req, res, next) => {
const SECONDS_PER_DAY = 86400;
try {
const deletedCount = await this.db.deleteChecksByTeamId(req.params.teamId);
return res.status(200).json({
success: true,
msg: successMessages.CHECK_DELETE,
data: { deletedCount },
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "deleteChecksByTeamId"));
}
};
try {
await updateChecksTTLBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
updateChecksTTL = async (req, res, next) => {
const SECONDS_PER_DAY = 86400;
try {
// Get user's teamId
const token = getTokenFromHeaders(req.headers);
const { jwtSecret } = req.settingsService.getSettings();
const { teamId } = jwt.verify(token, jwtSecret);
const ttl = parseInt(req.body.ttl, 10) * SECONDS_PER_DAY;
await req.db.updateChecksTTL(teamId, ttl);
return res.status(200).json({
success: true,
msg: successMessages.CHECK_UPDATE_TTL,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "updateTTL"));
}
};
try {
await updateChecksTTLBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
export {
createCheck,
getChecks,
getTeamChecks,
deleteChecks,
deleteChecksByTeamId,
updateChecksTTL,
};
try {
// Get user's teamId
const token = getTokenFromHeaders(req.headers);
const { jwtSecret } = this.settingsService.getSettings();
const { teamId } = jwt.verify(token, jwtSecret);
const ttl = parseInt(req.body.ttl, 10) * SECONDS_PER_DAY;
await this.db.updateChecksTTL(teamId, ttl);
return res.status(200).json({
success: true,
msg: successMessages.CHECK_UPDATE_TTL,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "updateTTL"));
}
};
}
export default CheckController;
+74 -67
View File
@@ -4,83 +4,90 @@ import {
inviteVerificationBodyValidation,
} from "../validation/joi.js";
import logger from "../utils/logger.js";
import dotenv from "dotenv";
import jwt from "jsonwebtoken";
import { handleError, handleValidationError } from "./controllerUtils.js";
import { getTokenFromHeaders } from "../utils/utils.js";
dotenv.config();
const SERVICE_NAME = "inviteController";
/**
* Issues an invitation to a new user. Only admins can invite new users. An invitation token is created and sent via email.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.headers - The headers of the request.
* @property {string} req.headers.authorization - The authorization header containing the JWT token.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.email - The email of the user to be invited.
* @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 sending of the invitation, and the invitation token.
* @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;
class InviteController {
constructor(db, settingsService, emailService) {
this.db = db;
this.settingsService = settingsService;
this.emailService = emailService;
}
/**
* Issues an invitation to a new user. Only admins can invite new users. An invitation token is created and sent via email.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.headers - The headers of the request.
* @property {string} req.headers.authorization - The authorization header containing the JWT token.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.email - The email of the user to be invited.
* @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 sending of the invitation, and the invitation token.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
issueInvitation = async (req, res, next) => {
try {
await inviteRoleValidation.validateAsync({ roles: role });
await inviteBodyValidation.validateAsync(req.body);
// 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) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
const inviteToken = await this.db.requestInviteToken({ ...req.body });
const { clientHost } = this.settingsService.getSettings();
this.emailService
.buildAndSendEmail(
"employeeActivationTemplate",
{
name: firstname,
link: `${clientHost}/register/${inviteToken.token}`,
},
req.body.email,
"Welcome to Uptime Monitor"
)
.catch((error) => {
logger.error({
message: error.message,
service: SERVICE_NAME,
method: "issueInvitation",
stack: error.stack,
});
});
return res
.status(200)
.json({ success: true, msg: "Invite sent", data: inviteToken });
} catch (error) {
next(handleError(error, SERVICE_NAME, "inviteController"));
}
};
inviteVerifyController = async (req, res, next) => {
try {
await inviteVerificationBodyValidation.validateAsync(req.body);
} catch (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({
message: error.message,
service: SERVICE_NAME,
method: "issueInvitation",
stack: error.stack,
});
});
try {
const invite = await this.db.getInviteToken(req.body.token);
res.status(200).json({ status: "success", msg: "Invite verified", data: invite });
} catch (error) {
next(handleError(error, SERVICE_NAME, "inviteVerifyController"));
}
};
}
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) {
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) {
next(handleError(error, SERVICE_NAME, "inviteVerifyController"));
}
};
export { issueInvitation, inviteVerifyController };
export default InviteController;
+139 -140
View File
@@ -11,157 +11,156 @@ import jwt from "jsonwebtoken";
import { getTokenFromHeaders } from "../utils/utils.js";
import { successMessages } from "../utils/messages.js";
import { handleValidationError, handleError } from "./controllerUtils.js";
const SERVICE_NAME = "maintenanceWindowController";
const createMaintenanceWindows = async (req, res, next) => {
try {
await createMaintenanceWindowBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
class MaintenanceWindowController {
constructor(db, settingsService) {
this.db = db;
this.settingsService = settingsService;
}
try {
const token = getTokenFromHeaders(req.headers);
const { jwtSecret } = req.settingsService.getSettings();
const { teamId } = jwt.verify(token, jwtSecret);
const monitorIds = req.body.monitors;
const dbTransactions = monitorIds.map((monitorId) => {
return req.db.createMaintenanceWindow({
teamId,
monitorId,
name: req.body.name,
active: req.body.active ? req.body.active : true,
repeat: req.body.repeat,
start: req.body.start,
end: req.body.end,
createMaintenanceWindows = async (req, res, next) => {
try {
await createMaintenanceWindowBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const token = getTokenFromHeaders(req.headers);
const { jwtSecret } = this.settingsService.getSettings();
const { teamId } = jwt.verify(token, jwtSecret);
const monitorIds = req.body.monitors;
const dbTransactions = monitorIds.map((monitorId) => {
return this.db.createMaintenanceWindow({
teamId,
monitorId,
name: req.body.name,
active: req.body.active ? req.body.active : true,
repeat: req.body.repeat,
start: req.body.start,
end: req.body.end,
});
});
});
await Promise.all(dbTransactions);
return res.status(201).json({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_CREATE,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "createMaintenanceWindow"));
}
};
await Promise.all(dbTransactions);
return res.status(201).json({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_CREATE,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "createMaintenanceWindow"));
}
};
const getMaintenanceWindowById = async (req, res, next) => {
try {
await getMaintenanceWindowByIdParamValidation.validateAsync(req.params);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const maintenanceWindow = await req.db.getMaintenanceWindowById(req.params.id);
return res.status(200).json({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_GET_BY_ID,
data: maintenanceWindow,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMaintenanceWindowById"));
}
};
getMaintenanceWindowById = async (req, res, next) => {
try {
await getMaintenanceWindowByIdParamValidation.validateAsync(req.params);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const maintenanceWindow = await this.db.getMaintenanceWindowById(req.params.id);
return res.status(200).json({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_GET_BY_ID,
data: maintenanceWindow,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMaintenanceWindowById"));
}
};
const getMaintenanceWindowsByTeamId = async (req, res, next) => {
try {
await getMaintenanceWindowsByTeamIdQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
getMaintenanceWindowsByTeamId = async (req, res, next) => {
try {
await getMaintenanceWindowsByTeamIdQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const token = getTokenFromHeaders(req.headers);
const { jwtSecret } = req.settingsService.getSettings();
const { teamId } = jwt.verify(token, jwtSecret);
const maintenanceWindows = await req.db.getMaintenanceWindowsByTeamId(
teamId,
req.query
);
try {
const token = getTokenFromHeaders(req.headers);
const { jwtSecret } = this.settingsService.getSettings();
const { teamId } = jwt.verify(token, jwtSecret);
const maintenanceWindows = await this.db.getMaintenanceWindowsByTeamId(
teamId,
req.query
);
return res.status(200).json({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_GET_BY_TEAM,
data: maintenanceWindows,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMaintenanceWindowsByUserId"));
}
};
return res.status(200).json({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_GET_BY_TEAM,
data: maintenanceWindows,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMaintenanceWindowsByUserId"));
}
};
const getMaintenanceWindowsByMonitorId = async (req, res, next) => {
try {
await getMaintenanceWindowsByMonitorIdParamValidation.validateAsync(req.params);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
getMaintenanceWindowsByMonitorId = async (req, res, next) => {
try {
await getMaintenanceWindowsByMonitorIdParamValidation.validateAsync(req.params);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const maintenanceWindows = await req.db.getMaintenanceWindowsByMonitorId(
req.params.monitorId
);
try {
const maintenanceWindows = await this.db.getMaintenanceWindowsByMonitorId(
req.params.monitorId
);
return res.status(200).json({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_GET_BY_USER,
data: maintenanceWindows,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMaintenanceWindowsByMonitorId"));
}
};
return res.status(200).json({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_GET_BY_USER,
data: maintenanceWindows,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMaintenanceWindowsByMonitorId"));
}
};
const deleteMaintenanceWindow = async (req, res, next) => {
try {
await deleteMaintenanceWindowByIdParamValidation.validateAsync(req.params);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
await req.db.deleteMaintenanceWindowById(req.params.id);
return res.status(200).json({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_DELETE,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "deleteMaintenanceWindow"));
}
};
deleteMaintenanceWindow = async (req, res, next) => {
try {
await deleteMaintenanceWindowByIdParamValidation.validateAsync(req.params);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
await this.db.deleteMaintenanceWindowById(req.params.id);
return res.status(200).json({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_DELETE,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "deleteMaintenanceWindow"));
}
};
const editMaintenanceWindow = async (req, res, next) => {
try {
await editMaintenanceWindowByIdParamValidation.validateAsync(req.params);
await editMaintenanceByIdWindowBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const editedMaintenanceWindow = await req.db.editMaintenanceWindowById(
req.params.id,
req.body
);
return res.status(200).json({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_EDIT,
data: editedMaintenanceWindow,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "editMaintenanceWindow"));
}
};
editMaintenanceWindow = async (req, res, next) => {
try {
await editMaintenanceWindowByIdParamValidation.validateAsync(req.params);
await editMaintenanceByIdWindowBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const editedMaintenanceWindow = await this.db.editMaintenanceWindowById(
req.params.id,
req.body
);
return res.status(200).json({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_EDIT,
data: editedMaintenanceWindow,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "editMaintenanceWindow"));
}
};
}
export {
createMaintenanceWindows,
getMaintenanceWindowById,
getMaintenanceWindowsByTeamId,
getMaintenanceWindowsByMonitorId,
deleteMaintenanceWindow,
editMaintenanceWindow,
};
export default MaintenanceWindowController;
+470 -477
View File
@@ -14,7 +14,7 @@ import {
getCertificateParamValidation,
} from "../validation/joi.js";
import sslChecker from "ssl-checker";
import { errorMessages, successMessages } from "../utils/messages.js";
import { successMessages } from "../utils/messages.js";
import jwt from "jsonwebtoken";
import { getTokenFromHeaders } from "../utils/utils.js";
import logger from "../utils/logger.js";
@@ -23,510 +23,503 @@ import axios from "axios";
const SERVICE_NAME = "monitorController";
/**
* Returns all monitors
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @param {function} next
* @returns {Promise<Express.Response>}
* @throws {Error}
*/
const getAllMonitors = async (req, res, next) => {
try {
const monitors = await req.db.getAllMonitors();
return res.status(200).json({
success: true,
msg: successMessages.MONITOR_GET_ALL,
data: monitors,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getAllMonitors"));
}
};
/**
* Returns all monitors with uptime stats for 1,7,30, and 90 days
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @param {function} next
* @returns {Promise<Express.Response>}
* @throws {Error}
*/
const getAllMonitorsWithUptimeStats = async (req, res, next) => {
try {
const monitors = await req.db.getAllMonitorsWithUptimeStats();
return res.status(200).json({
success: true,
msg: successMessages.MONITOR_GET_ALL,
data: monitors,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getAllMonitorsWithUptimeStats"));
}
};
/**
* Returns monitor stats for monitor with matching ID
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @param {function} next
* @returns {Promise<Express.Response>}
* @throws {Error}
*/
const getMonitorStatsById = async (req, res, next) => {
try {
await getMonitorStatsByIdParamValidation.validateAsync(req.params);
await getMonitorStatsByIdQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
class MonitorController {
constructor(db, settingsService, jobQueue) {
this.db = db;
this.settingsService = settingsService;
this.jobQueue = jobQueue;
}
try {
const monitorStats = await req.db.getMonitorStatsById(req);
return res.status(200).json({
success: true,
msg: successMessages.MONITOR_STATS_BY_ID,
data: monitorStats,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMonitorStatsById"));
}
};
/**
* Returns all monitors
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @param {function} next
* @returns {Promise<Express.Response>}
* @throws {Error}
*/
getAllMonitors = async (req, res, next) => {
try {
const monitors = await this.db.getAllMonitors();
return res.status(200).json({
success: true,
msg: successMessages.MONITOR_GET_ALL,
data: monitors,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getAllMonitors"));
}
};
const getMonitorCertificate = async (req, res, next, fetchMonitorCertificate) => {
try {
await getCertificateParamValidation.validateAsync(req.params);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
}
/**
* Returns all monitors with uptime stats for 1,7,30, and 90 days
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @param {function} next
* @returns {Promise<Express.Response>}
* @throws {Error}
*/
getAllMonitorsWithUptimeStats = async (req, res, next) => {
try {
const monitors = await this.db.getAllMonitorsWithUptimeStats();
return res.status(200).json({
success: true,
msg: successMessages.MONITOR_GET_ALL,
data: monitors,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getAllMonitorsWithUptimeStats"));
}
};
try {
const { monitorId } = req.params;
const monitor = await req.db.getMonitorById(monitorId);
const certificate = await fetchMonitorCertificate(sslChecker, monitor);
/**
* Returns monitor stats for monitor with matching ID
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @param {function} next
* @returns {Promise<Express.Response>}
* @throws {Error}
*/
getMonitorStatsById = async (req, res, next) => {
try {
await getMonitorStatsByIdParamValidation.validateAsync(req.params);
await getMonitorStatsByIdQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
return res.status(200).json({
success: true,
msg: successMessages.MONITOR_CERTIFICATE,
data: {
certificateDate: new Date(certificate.validTo),
},
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMonitorCertificate"));
}
};
try {
const monitorStats = await this.db.getMonitorStatsById(req);
return res.status(200).json({
success: true,
msg: successMessages.MONITOR_STATS_BY_ID,
data: monitorStats,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMonitorStatsById"));
}
};
/**
* Retrieves a monitor by its ID.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.params - The parameters of the request.
* @property {string} req.params.monitorId - The ID of the monitor to be retrieved.
* @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, and the retrieved monitor data.
* @throws {Error} If there is an error during the process, especially if the monitor is not found (404) or if there is a validation error (422).
*/
const getMonitorById = async (req, res, next) => {
try {
await getMonitorByIdParamValidation.validateAsync(req.params);
await getMonitorByIdQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
getMonitorCertificate = async (req, res, next, fetchMonitorCertificate) => {
try {
await getCertificateParamValidation.validateAsync(req.params);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
}
try {
const monitor = await req.db.getMonitorById(req.params.monitorId);
return res.status(200).json({
success: true,
msg: successMessages.MONITOR_GET_BY_ID,
data: monitor,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMonitorById"));
}
};
try {
const { monitorId } = req.params;
const monitor = await this.db.getMonitorById(monitorId);
const certificate = await fetchMonitorCertificate(sslChecker, monitor);
/**
* Retrieves all monitors and a summary for a team based on the team ID.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.params - The parameters of the request.
* @property {string} req.params.teamId - The ID of the team.
* @property {Object} req.query - The query parameters of the request.
* @property {string} req.query.type - The type of the request.
* @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, and the data containing the monitors and summary for the team.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
const getMonitorsAndSummaryByTeamId = async (req, res, next) => {
try {
await getMonitorsAndSummaryByTeamIdParamValidation.validateAsync(req.params);
await getMonitorsAndSummaryByTeamIdQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
return res.status(200).json({
success: true,
msg: successMessages.MONITOR_CERTIFICATE,
data: {
certificateDate: new Date(certificate.validTo),
},
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMonitorCertificate"));
}
};
try {
const { teamId } = req.params;
const { type } = req.query;
const monitorsSummary = await req.db.getMonitorsAndSummaryByTeamId(teamId, type);
return res.status(200).json({
success: true,
msg: successMessages.MONITOR_GET_BY_USER_ID(teamId),
data: monitorsSummary,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMonitorsAndSummaryByTeamId"));
}
};
/**
* Retrieves a monitor by its ID.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.params - The parameters of the request.
* @property {string} req.params.monitorId - The ID of the monitor to be retrieved.
* @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, and the retrieved monitor data.
* @throws {Error} If there is an error during the process, especially if the monitor is not found (404) or if there is a validation error (422).
*/
getMonitorById = async (req, res, next) => {
try {
await getMonitorByIdParamValidation.validateAsync(req.params);
await getMonitorByIdQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
/**
* Retrieves all monitors associated with a team by the team's ID.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.params - The parameters of the request.
* @property {string} req.params.teamId - The ID of the team.
* @property {Object} req.query - The query parameters of the request.
* @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, and the data containing the monitors for the team.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
const getMonitorsByTeamId = async (req, res, next) => {
try {
await getMonitorsByTeamIdValidation.validateAsync(req.params);
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const monitor = await this.db.getMonitorById(req.params.monitorId);
return res.status(200).json({
success: true,
msg: successMessages.MONITOR_GET_BY_ID,
data: monitor,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMonitorById"));
}
};
try {
const teamId = req.params.teamId;
const monitors = await req.db.getMonitorsByTeamId(req, res);
return res.status(200).json({
success: true,
msg: successMessages.MONITOR_GET_BY_USER_ID(teamId),
data: monitors,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMonitorsByTeamId"));
next(error);
}
};
/**
* Retrieves all monitors and a summary for a team based on the team ID.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.params - The parameters of the request.
* @property {string} req.params.teamId - The ID of the team.
* @property {Object} req.query - The query parameters of the request.
* @property {string} req.query.type - The type of the request.
* @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, and the data containing the monitors and summary for the team.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
getMonitorsAndSummaryByTeamId = async (req, res, next) => {
try {
await getMonitorsAndSummaryByTeamIdParamValidation.validateAsync(req.params);
await getMonitorsAndSummaryByTeamIdQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
/**
* Creates a new monitor and adds it to the job queue.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {Array} req.body.notifications - The notifications associated with the monitor.
* @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 monitor, and the created monitor data.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
const createMonitor = async (req, res, next) => {
try {
await createMonitorBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const { teamId } = req.params;
const { type } = req.query;
const monitorsSummary = await this.db.getMonitorsAndSummaryByTeamId(teamId, type);
return res.status(200).json({
success: true,
msg: successMessages.MONITOR_GET_BY_USER_ID(teamId),
data: monitorsSummary,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMonitorsAndSummaryByTeamId"));
}
};
try {
const notifications = req.body.notifications;
const monitor = await req.db.createMonitor(req, res);
/**
* Retrieves all monitors associated with a team by the team's ID.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.params - The parameters of the request.
* @property {string} req.params.teamId - The ID of the team.
* @property {Object} req.query - The query parameters of the request.
* @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, and the data containing the monitors for the team.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
getMonitorsByTeamId = async (req, res, next) => {
try {
await getMonitorsByTeamIdValidation.validateAsync(req.params);
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
if (notifications && notifications.length > 0) {
monitor.notifications = await Promise.all(
notifications.map(async (notification) => {
notification.monitorId = monitor._id;
return await req.db.createNotification(notification);
try {
const teamId = req.params.teamId;
const monitors = await this.db.getMonitorsByTeamId(req, res);
return res.status(200).json({
success: true,
msg: successMessages.MONITOR_GET_BY_USER_ID(teamId),
data: monitors,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMonitorsByTeamId"));
next(error);
}
};
/**
* Creates a new monitor and adds it to the job queue.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {Array} req.body.notifications - The notifications associated with the monitor.
* @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 monitor, and the created monitor data.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
createMonitor = async (req, res, next) => {
try {
await createMonitorBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const notifications = req.body.notifications;
const monitor = await this.db.createMonitor(req, res);
if (notifications && notifications.length > 0) {
monitor.notifications = await Promise.all(
notifications.map(async (notification) => {
notification.monitorId = monitor._id;
return await this.db.createNotification(notification);
})
);
}
await monitor.save();
// Add monitor to job queue
this.jobQueue.addJob(monitor._id, monitor);
return res.status(201).json({
success: true,
msg: successMessages.MONITOR_CREATE,
data: monitor,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "createMonitor"));
}
};
/**
* Checks if the endpoint can be resolved
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.query - The query parameters of the request.
* @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, and the resolution result.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
checkEndpointResolution = async (req, res, next) => {
try {
await getMonitorURLByQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const { monitorURL } = req.query;
const parsedUrl = new URL(monitorURL);
const response = await axios.get(parsedUrl, {
timeout: 5000,
validateStatus: () => true,
});
return res.status(200).json({
success: true,
code: response.status,
statusText: response.statusText,
msg: `URL resolved successfully`,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "checkEndpointResolution"));
}
};
/**
* Deletes a monitor by its ID and also deletes associated checks, alerts, and notifications.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.params - The parameters of the request.
* @property {string} req.params.monitorId - The ID of the monitor to be deleted.
* @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 deletion of the monitor.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422) or an error in deleting associated records.
*/
deleteMonitor = async (req, res, next) => {
try {
await getMonitorByIdParamValidation.validateAsync(req.params);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const monitor = await this.db.deleteMonitor(req, res, next);
// Delete associated checks,alerts,and notifications
try {
await this.jobQueue.deleteJob(monitor);
await this.db.deleteChecks(monitor._id);
await this.db.deletePageSpeedChecksByMonitorId(monitor._id);
await this.db.deleteNotificationsByMonitorId(monitor._id);
await this.db.deleteHardwareChecksByMonitorId(monitor._id);
} catch (error) {
logger.error({
message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`,
service: SERVICE_NAME,
method: "deleteMonitor",
stack: error.stack,
});
}
return res.status(200).json({ success: true, msg: successMessages.MONITOR_DELETE });
} catch (error) {
next(handleError(error, SERVICE_NAME, "deleteMonitor"));
}
};
/**
* Deletes all monitors associated with a team.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.headers - The headers of the request.
* @property {string} req.headers.authorization - The authorization header containing the JWT token.
* @param {Object} res - The Express response object.
* @param {function} next
* @returns {Object} The response object with a success status and a message indicating the number of deleted monitors.
* @throws {Error} If there is an error during the deletion process.
*/
deleteAllMonitors = async (req, res, next) => {
try {
const token = getTokenFromHeaders(req.headers);
const { jwtSecret } = this.settingsService.getSettings();
const { teamId } = jwt.verify(token, jwtSecret);
const { monitors, deletedCount } = await this.db.deleteAllMonitors(teamId);
await Promise.all(
monitors.map(async (monitor) => {
try {
await this.jobQueue.deleteJob(monitor);
await this.db.deleteChecks(monitor._id);
await this.db.deletePageSpeedChecksByMonitorId(monitor._id);
await this.db.deleteNotificationsByMonitorId(monitor._id);
} catch (error) {
logger.error({
message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`,
service: SERVICE_NAME,
method: "deleteAllMonitors",
stack: error.stack,
});
}
})
);
}
await monitor.save();
// Add monitor to job queue
req.jobQueue.addJob(monitor._id, monitor);
return res.status(201).json({
success: true,
msg: successMessages.MONITOR_CREATE,
data: monitor,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "createMonitor"));
}
};
/**
* Checks if the endpoint can be resolved
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.query - The query parameters of the request.
* @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, and the resolution result.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
const checkEndpointResolution = async (req, res, next) => {
try {
await getMonitorURLByQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const { monitorURL } = req.query;
const parsedUrl = new URL(monitorURL);
const response = await axios.get(parsedUrl, {
timeout: 5000,
validateStatus: () => true,
});
return res.status(200).json({
success: true,
code: response.status,
statusText: response.statusText,
msg: `URL resolved successfully`,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "checkEndpointResolution"));
}
};
/**
* Deletes a monitor by its ID and also deletes associated checks, alerts, and notifications.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.params - The parameters of the request.
* @property {string} req.params.monitorId - The ID of the monitor to be deleted.
* @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 deletion of the monitor.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422) or an error in deleting associated records.
*/
const deleteMonitor = async (req, res, next) => {
try {
await getMonitorByIdParamValidation.validateAsync(req.params);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const monitor = await req.db.deleteMonitor(req, res, next);
// Delete associated checks,alerts,and notifications
try {
await req.jobQueue.deleteJob(monitor);
await req.db.deleteChecks(monitor._id);
await req.db.deletePageSpeedChecksByMonitorId(monitor._id);
await req.db.deleteNotificationsByMonitorId(monitor._id);
await req.db.deleteHardwareChecksByMonitorId(monitor._id);
return res
.status(200)
.json({ success: true, msg: `Deleted ${deletedCount} monitors` });
} catch (error) {
logger.error({
message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`,
service: SERVICE_NAME,
method: "deleteMonitor",
stack: error.stack,
});
next(handleError(error, SERVICE_NAME, "deleteAllMonitors"));
}
return res.status(200).json({ success: true, msg: successMessages.MONITOR_DELETE });
} catch (error) {
next(handleError(error, SERVICE_NAME, "deleteMonitor"));
}
};
};
/**
* Deletes all monitors associated with a team.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.headers - The headers of the request.
* @property {string} req.headers.authorization - The authorization header containing the JWT token.
* @param {Object} res - The Express response object.
* @param {function} next
* @returns {Object} The response object with a success status and a message indicating the number of deleted monitors.
* @throws {Error} If there is an error during the deletion process.
*/
const deleteAllMonitors = async (req, res, next) => {
try {
const token = getTokenFromHeaders(req.headers);
const { jwtSecret } = req.settingsService.getSettings();
const { teamId } = jwt.verify(token, jwtSecret);
const { monitors, deletedCount } = await req.db.deleteAllMonitors(teamId);
await Promise.all(
monitors.map(async (monitor) => {
try {
await req.jobQueue.deleteJob(monitor);
await req.db.deleteChecks(monitor._id);
await req.db.deletePageSpeedChecksByMonitorId(monitor._id);
await req.db.deleteNotificationsByMonitorId(monitor._id);
} catch (error) {
logger.error({
message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`,
service: SERVICE_NAME,
method: "deleteAllMonitors",
stack: error.stack,
});
}
})
);
return res
.status(200)
.json({ success: true, msg: `Deleted ${deletedCount} monitors` });
} catch (error) {
next(handleError(error, SERVICE_NAME, "deleteAllMonitors"));
}
};
/**
* Edits a monitor by its ID, updates its notifications, and updates its job in the job queue.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.params - The parameters of the request.
* @property {string} req.params.monitorId - The ID of the monitor to be edited.
* @property {Object} req.body - The body of the request.
* @property {Array} req.body.notifications - The notifications to be associated with the monitor.
* @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 editing of the monitor, and the edited monitor data.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
editMonitor = async (req, res, next) => {
try {
await getMonitorByIdParamValidation.validateAsync(req.params);
await editMonitorBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
/**
* Edits a monitor by its ID, updates its notifications, and updates its job in the job queue.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.params - The parameters of the request.
* @property {string} req.params.monitorId - The ID of the monitor to be edited.
* @property {Object} req.body - The body of the request.
* @property {Array} req.body.notifications - The notifications to be associated with the monitor.
* @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 editing of the monitor, and the edited monitor data.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
const editMonitor = async (req, res, next) => {
try {
await getMonitorByIdParamValidation.validateAsync(req.params);
await editMonitorBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const { monitorId } = req.params;
const monitorBeforeEdit = await this.db.getMonitorById(monitorId);
try {
const { monitorId } = req.params;
const monitorBeforeEdit = await req.db.getMonitorById(monitorId);
// Get notifications from the request body
const notifications = req.body.notifications;
// Get notifications from the request body
const notifications = req.body.notifications;
const editedMonitor = await this.db.editMonitor(monitorId, req.body);
const editedMonitor = await req.db.editMonitor(monitorId, req.body);
await this.db.deleteNotificationsByMonitorId(editedMonitor._id);
await req.db.deleteNotificationsByMonitorId(editedMonitor._id);
await Promise.all(
notifications &&
notifications.map(async (notification) => {
notification.monitorId = editedMonitor._id;
await this.db.createNotification(notification);
})
);
await Promise.all(
notifications &&
notifications.map(async (notification) => {
notification.monitorId = editedMonitor._id;
await req.db.createNotification(notification);
})
);
// Delete the old job(editedMonitor has the same ID as the old monitor)
await this.jobQueue.deleteJob(monitorBeforeEdit);
// Add the new job back to the queue
await this.jobQueue.addJob(editedMonitor._id, editedMonitor);
return res.status(200).json({
success: true,
msg: successMessages.MONITOR_EDIT,
data: editedMonitor,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "editMonitor"));
}
};
// Delete the old job(editedMonitor has the same ID as the old monitor)
await req.jobQueue.deleteJob(monitorBeforeEdit);
// Add the new job back to the queue
await req.jobQueue.addJob(editedMonitor._id, editedMonitor);
return res.status(200).json({
success: true,
msg: successMessages.MONITOR_EDIT,
data: editedMonitor,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "editMonitor"));
}
};
/**
* Pauses or resumes a monitor based on its current state.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.params - The parameters of the request.
* @property {string} req.params.monitorId - The ID of the monitor to be paused or resumed.
* @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 new state of the monitor, and the updated monitor data.
* @throws {Error} If there is an error during the process.
*/
pauseMonitor = async (req, res, next) => {
try {
await pauseMonitorParamValidation.validateAsync(req.params);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
}
/**
* Pauses or resumes a monitor based on its current state.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.params - The parameters of the request.
* @property {string} req.params.monitorId - The ID of the monitor to be paused or resumed.
* @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 new state of the monitor, and the updated monitor data.
* @throws {Error} If there is an error during the process.
*/
const pauseMonitor = async (req, res, next) => {
try {
await pauseMonitorParamValidation.validateAsync(req.params);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
}
try {
const monitor = await this.db.getMonitorById(req.params.monitorId);
monitor.isActive === true
? await this.jobQueue.deleteJob(monitor)
: await this.jobQueue.addJob(monitor._id, monitor);
try {
const monitor = await req.db.getMonitorById(req.params.monitorId);
monitor.isActive === true
? await req.jobQueue.deleteJob(monitor)
: await req.jobQueue.addJob(monitor._id, monitor);
monitor.isActive = !monitor.isActive;
monitor.status = undefined;
monitor.save();
return res.status(200).json({
success: true,
msg: monitor.isActive
? successMessages.MONITOR_RESUME
: successMessages.MONITOR_PAUSE,
data: monitor,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "pauseMonitor"));
}
};
monitor.isActive = !monitor.isActive;
monitor.status = undefined;
monitor.save();
return res.status(200).json({
success: true,
msg: monitor.isActive
? successMessages.MONITOR_RESUME
: successMessages.MONITOR_PAUSE,
data: monitor,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "pauseMonitor"));
}
};
/**
* Adds demo monitors for a team.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.headers - The headers of the request.
* @property {string} req.headers.authorization - The authorization header containing the JWT token.
* @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 addition of demo monitors, and the number of demo monitors added.
* @throws {Error} If there is an error during the process.
*/
addDemoMonitors = async (req, res, next) => {
try {
const token = getTokenFromHeaders(req.headers);
const { jwtSecret } = this.settingsService.getSettings();
const { _id, teamId } = jwt.verify(token, jwtSecret);
const demoMonitors = await this.db.addDemoMonitors(_id, teamId);
await Promise.all(
demoMonitors.map((monitor) => this.jobQueue.addJob(monitor._id, monitor))
);
/**
* Adds demo monitors for a team.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.headers - The headers of the request.
* @property {string} req.headers.authorization - The authorization header containing the JWT token.
* @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 addition of demo monitors, and the number of demo monitors added.
* @throws {Error} If there is an error during the process.
*/
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 Promise.all(
demoMonitors.map((monitor) => req.jobQueue.addJob(monitor._id, monitor))
);
return res.status(200).json({
success: true,
msg: successMessages.MONITOR_DEMO_ADDED,
data: demoMonitors.length,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "addDemoMonitors"));
}
};
}
return res.status(200).json({
success: true,
msg: successMessages.MONITOR_DEMO_ADDED,
data: demoMonitors.length,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "addDemoMonitors"));
}
};
export {
getAllMonitors,
getAllMonitorsWithUptimeStats,
getMonitorStatsById,
getMonitorCertificate,
getMonitorById,
getMonitorsAndSummaryByTeamId,
getMonitorsByTeamId,
createMonitor,
checkEndpointResolution,
deleteMonitor,
deleteAllMonitors,
editMonitor,
pauseMonitor,
addDemoMonitors,
};
export default MonitorController;
+55 -48
View File
@@ -1,57 +1,64 @@
import { handleError } from "./controllerUtils.js";
import { errorMessages, successMessages } from "../utils/messages.js";
import { successMessages } from "../utils/messages.js";
const SERVICE_NAME = "JobQueueController";
const getMetrics = async (req, res, next) => {
try {
const metrics = await req.jobQueue.getMetrics();
res.status(200).json({
success: true,
msg: successMessages.QUEUE_GET_METRICS,
data: metrics,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMetrics"));
return;
class JobQueueController {
constructor(jobQueue) {
this.jobQueue = jobQueue;
}
};
const getJobs = async (req, res, next) => {
try {
const jobs = await req.jobQueue.getJobStats();
return res.status(200).json({
success: true,
msg: successMessages.QUEUE_GET_METRICS,
data: jobs,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getJobs"));
return;
}
};
getMetrics = async (req, res, next) => {
try {
const metrics = await this.jobQueue.getMetrics();
res.status(200).json({
success: true,
msg: successMessages.QUEUE_GET_METRICS,
data: metrics,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMetrics"));
return;
}
};
const addJob = async (req, res, next) => {
try {
await req.jobQueue.addJob(Math.random().toString(36).substring(7));
return res.status(200).json({
success: true,
msg: successMessages.QUEUE_ADD_JOB,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "addJob"));
return;
}
};
getJobs = async (req, res, next) => {
try {
const jobs = await this.jobQueue.getJobStats();
return res.status(200).json({
success: true,
msg: successMessages.QUEUE_GET_METRICS,
data: jobs,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getJobs"));
return;
}
};
const obliterateQueue = async (req, res, next) => {
try {
await req.jobQueue.obliterate();
return res.status(200).json({ success: true, msg: successMessages.QUEUE_OBLITERATE });
} catch (error) {
next(handleError(error, SERVICE_NAME, "obliterateQueue"));
return;
}
};
addJob = async (req, res, next) => {
try {
await this.jobQueue.addJob(Math.random().toString(36).substring(7));
return res.status(200).json({
success: true,
msg: successMessages.QUEUE_ADD_JOB,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "addJob"));
return;
}
};
export { getMetrics, getJobs, addJob, obliterateQueue };
obliterateQueue = async (req, res, next) => {
try {
await this.jobQueue.obliterate();
return res
.status(200)
.json({ success: true, msg: successMessages.QUEUE_OBLITERATE });
} catch (error) {
next(handleError(error, SERVICE_NAME, "obliterateQueue"));
return;
}
};
}
export default JobQueueController;
+41 -34
View File
@@ -3,40 +3,47 @@ import { updateAppSettingsBodyValidation } from "../validation/joi.js";
import { handleValidationError, handleError } from "./controllerUtils.js";
const SERVICE_NAME = "SettingsController";
const getAppSettings = async (req, res, next) => {
try {
const settings = { ...(await req.settingsService.getSettings()) };
delete settings.jwtSecret;
return res.status(200).json({
success: true,
msg: successMessages.GET_APP_SETTINGS,
data: settings,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getAppSettings"));
}
};
const updateAppSettings = async (req, res, next) => {
try {
await updateAppSettingsBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
class SettingsController {
constructor(db, settingsService) {
this.db = db;
this.settingsService = settingsService;
}
try {
await req.db.updateAppSettings(req.body);
const updatedSettings = { ...(await req.settingsService.reloadSettings()) };
delete updatedSettings.jwtSecret;
return res.status(200).json({
success: true,
msg: successMessages.UPDATE_APP_SETTINGS,
data: updatedSettings,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "updateAppSettings"));
}
};
getAppSettings = async (req, res, next) => {
try {
const settings = { ...(await this.settingsService.getSettings()) };
delete settings.jwtSecret;
return res.status(200).json({
success: true,
msg: successMessages.GET_APP_SETTINGS,
data: settings,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getAppSettings"));
}
};
export { getAppSettings, updateAppSettings };
updateAppSettings = async (req, res, next) => {
try {
await updateAppSettingsBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
await this.db.updateAppSettings(req.body);
const updatedSettings = { ...(await this.settingsService.reloadSettings()) };
delete updatedSettings.jwtSecret;
return res.status(200).json({
success: true,
msg: successMessages.UPDATE_APP_SETTINGS,
data: updatedSettings,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "updateAppSettings"));
}
};
}
export default SettingsController;
+42 -37
View File
@@ -7,45 +7,50 @@ import { successMessages } from "../utils/messages.js";
const SERVICE_NAME = "statusPageController";
const createStatusPage = async (req, res, next) => {
try {
await createStatusPageBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
class StatusPageController {
constructor(db) {
this.db = db;
}
try {
const statusPage = await req.db.createStatusPage(req.body);
return res.status(200).json({
success: true,
msg: successMessages.STATUS_PAGE_CREATE,
data: statusPage,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "createStatusPage"));
}
};
createStatusPage = async (req, res, next) => {
try {
await createStatusPageBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
const getStatusPageByUrl = async (req, res, next) => {
try {
await getStatusPageParamValidation.validateAsync(req.params);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const statusPage = await this.db.createStatusPage(req.body);
return res.status(200).json({
success: true,
msg: successMessages.STATUS_PAGE_CREATE,
data: statusPage,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "createStatusPage"));
}
};
getStatusPageByUrl = async (req, res, next) => {
try {
await getStatusPageParamValidation.validateAsync(req.params);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const { url } = req.params;
const statusPage = await req.db.getStatusPageByUrl(url);
return res.status(200).json({
success: true,
msg: successMessages.STATUS_PAGE_BY_URL,
data: statusPage,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getStatusPage"));
}
};
try {
const { url } = req.params;
const statusPage = await this.db.getStatusPageByUrl(url);
return res.status(200).json({
success: true,
msg: successMessages.STATUS_PAGE_BY_URL,
data: statusPage,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getStatusPage"));
}
};
}
export { createStatusPage, getStatusPageByUrl };
export default StatusPageController;
+4 -1
View File
@@ -28,12 +28,15 @@ const MonitorSchema = mongoose.Schema(
type: {
type: String,
required: true,
enum: ["http", "ping", "pagespeed", "hardware", "docker"],
enum: ["http", "ping", "pagespeed", "hardware", "docker", "port"],
},
url: {
type: String,
required: true,
},
port: {
type: Number,
},
isActive: {
type: Boolean,
default: true,
+76 -181
View File
@@ -2,234 +2,129 @@ import mongoose from "mongoose";
import UserModel from "../models/User.js";
import AppSettings from "../models/AppSettings.js";
import logger from "../../utils/logger.js";
const SERVICE_NAME = "MongoDB";
//****************************************
// DB Connection
//****************************************
const connect = async () => {
try {
const connectionString =
process.env.DB_CONNECTION_STRING || "mongodb://localhost:27017/uptime_db";
await mongoose.connect(connectionString);
// If there are no AppSettings, create one
let appSettings = await AppSettings.find();
if (appSettings.length === 0) {
appSettings = new AppSettings({});
await appSettings.save();
}
logger.info({ message: "Connected to MongoDB" });
} catch (error) {
logger.error({
message: error.message,
service: SERVICE_NAME,
method: "connect",
stack: error.stack,
});
throw error;
}
};
const disconnect = async () => {
try {
logger.info({ message: "Disconnecting from MongoDB" });
await mongoose.disconnect();
logger.info({ message: "Disconnected from MongoDB" });
return;
} catch (error) {
logger.error({
message: error.message,
service: SERVICE_NAME,
method: "disconnect",
stack: error.stack,
});
}
};
const checkSuperadmin = async (req, res) => {
try {
const superAdmin = await UserModel.findOne({ role: "superadmin" });
if (superAdmin !== null) {
return true;
}
return false;
} catch (error) {
throw error;
}
};
//****************************************
// User Operations
//****************************************
import {
insertUser,
getUserByEmail,
updateUser,
deleteUser,
deleteTeam,
deleteAllOtherUsers,
getAllUsers,
logoutUser,
} from "./modules/userModule.js";
import * as userModule from "./modules/userModule.js";
//****************************************
// Invite Token Operations
//****************************************
import {
requestInviteToken,
getInviteToken,
getInviteTokenAndDelete,
} from "./modules/inviteModule.js";
import * as inviteModule from "./modules/inviteModule.js";
//****************************************
// Recovery Operations
//****************************************
import {
requestRecoveryToken,
validateRecoveryToken,
resetPassword,
} from "./modules/recoveryModule.js";
import * as recoveryModule from "./modules/recoveryModule.js";
//****************************************
// Monitors
//****************************************
import {
getAllMonitors,
getAllMonitorsWithUptimeStats,
getMonitorStatsById,
getMonitorById,
getMonitorsAndSummaryByTeamId,
getMonitorsByTeamId,
createMonitor,
deleteMonitor,
deleteAllMonitors,
deleteMonitorsByUserId,
editMonitor,
addDemoMonitors,
} from "./modules/monitorModule.js";
import * as monitorModule from "./modules/monitorModule.js";
//****************************************
// Page Speed Checks
//****************************************
import {
createPageSpeedCheck,
deletePageSpeedChecksByMonitorId,
} from "./modules/pageSpeedCheckModule.js";
import * as pageSpeedCheckModule from "./modules/pageSpeedCheckModule.js";
//****************************************
// Hardware Checks
//****************************************
import {
createHardwareCheck,
deleteHardwareChecksByMonitorId,
} from "./modules/hardwareCheckModule.js";
import * as hardwareCheckModule from "./modules/hardwareCheckModule.js";
//****************************************
// Checks
//****************************************
import {
createCheck,
getChecksCount,
getChecks,
getTeamChecks,
deleteChecks,
deleteChecksByTeamId,
updateChecksTTL,
} from "./modules/checkModule.js";
import * as checkModule from "./modules/checkModule.js";
//****************************************
// Maintenance Window
//****************************************
import {
createMaintenanceWindow,
getMaintenanceWindowById,
getMaintenanceWindowsByTeamId,
getMaintenanceWindowsByMonitorId,
deleteMaintenanceWindowById,
deleteMaintenanceWindowByMonitorId,
deleteMaintenanceWindowByUserId,
editMaintenanceWindowById,
} from "./modules/maintenanceWindowModule.js";
import * as maintenanceWindowModule from "./modules/maintenanceWindowModule.js";
//****************************************
// Notifications
//****************************************
import {
createNotification,
getNotificationsByMonitorId,
deleteNotificationsByMonitorId,
} from "./modules/notificationModule.js";
import * as notificationModule from "./modules/notificationModule.js";
//****************************************
// AppSettings
//****************************************
import { getAppSettings, updateAppSettings } from "./modules/settingsModule.js";
import * as settingsModule from "./modules/settingsModule.js";
//****************************************
// Status Page
//****************************************
import { createStatusPage, getStatusPageByUrl } from "./modules/statusPageModule.js";
import * as statusPageModule from "./modules/statusPageModule.js";
export default {
connect,
disconnect,
insertUser,
getUserByEmail,
updateUser,
deleteUser,
deleteTeam,
deleteAllOtherUsers,
getAllUsers,
logoutUser,
requestInviteToken,
getInviteToken,
getInviteTokenAndDelete,
requestRecoveryToken,
validateRecoveryToken,
resetPassword,
checkSuperadmin,
getAllMonitors,
getAllMonitorsWithUptimeStats,
getMonitorStatsById,
getMonitorById,
getMonitorsAndSummaryByTeamId,
getMonitorsByTeamId,
createMonitor,
deleteMonitor,
deleteAllMonitors,
editMonitor,
addDemoMonitors,
createCheck,
getChecksCount,
getChecks,
getTeamChecks,
deleteChecks,
deleteChecksByTeamId,
updateChecksTTL,
deleteMonitorsByUserId,
createPageSpeedCheck,
deletePageSpeedChecksByMonitorId,
createHardwareCheck,
deleteHardwareChecksByMonitorId,
createMaintenanceWindow,
getMaintenanceWindowsByTeamId,
getMaintenanceWindowById,
getMaintenanceWindowsByMonitorId,
deleteMaintenanceWindowById,
deleteMaintenanceWindowByMonitorId,
deleteMaintenanceWindowByUserId,
editMaintenanceWindowById,
createNotification,
getNotificationsByMonitorId,
deleteNotificationsByMonitorId,
getAppSettings,
updateAppSettings,
createStatusPage,
getStatusPageByUrl,
};
class MongoDB {
static SERVICE_NAME = "MongoDB";
constructor() {
Object.assign(this, userModule);
Object.assign(this, inviteModule);
Object.assign(this, recoveryModule);
Object.assign(this, monitorModule);
Object.assign(this, pageSpeedCheckModule);
Object.assign(this, hardwareCheckModule);
Object.assign(this, checkModule);
Object.assign(this, maintenanceWindowModule);
Object.assign(this, notificationModule);
Object.assign(this, settingsModule);
Object.assign(this, statusPageModule);
}
connect = async () => {
try {
const connectionString =
process.env.DB_CONNECTION_STRING || "mongodb://localhost:27017/uptime_db";
await mongoose.connect(connectionString);
// If there are no AppSettings, create one
let appSettings = await AppSettings.find();
if (appSettings.length === 0) {
appSettings = new AppSettings({});
await appSettings.save();
}
logger.info({ message: "Connected to MongoDB" });
} catch (error) {
logger.error({
message: error.message,
service: this.SERVICE_NAME,
method: "connect",
stack: error.stack,
});
throw error;
}
};
disconnect = async () => {
try {
logger.info({ message: "Disconnecting from MongoDB" });
await mongoose.disconnect();
logger.info({ message: "Disconnected from MongoDB" });
return;
} catch (error) {
logger.error({
message: error.message,
service: this.SERVICE_NAME,
method: "disconnect",
stack: error.stack,
});
}
};
checkSuperadmin = async (req, res) => {
const superAdmin = await UserModel.findOne({ role: "superadmin" });
if (superAdmin !== null) {
return true;
}
return false;
};
}
export default MongoDB;
+7 -1
View File
@@ -21,6 +21,7 @@ const CHECK_MODEL_LOOKUP = {
http: Check,
ping: Check,
docker: Check,
port: Check,
pagespeed: PageSpeedCheck,
hardware: HardwareCheck,
};
@@ -351,7 +352,12 @@ const getMonitorStatsById = async (req) => {
),
};
if (monitor.type === "http" || monitor.type === "ping" || monitor.type === "docker") {
if (
monitor.type === "http" ||
monitor.type === "ping" ||
monitor.type === "docker" ||
monitor.type === "port"
) {
// HTTP/PING Specific stats
monitorStats.periodAvgResponseTime = getAverageResponseTime(checksForDateRange);
monitorStats.periodUptime = getUptimePercentage(checksForDateRange);
+151 -114
View File
@@ -8,16 +8,31 @@ import cors from "cors";
import logger from "./utils/logger.js";
import { verifyJWT } from "./middleware/verifyJWT.js";
import { handleErrors } from "./middleware/handleErrors.js";
import authRouter from "./routes/authRoute.js";
import inviteRouter from "./routes/inviteRoute.js";
import monitorRouter from "./routes/monitorRoute.js";
import checkRouter from "./routes/checkRoute.js";
import maintenanceWindowRouter from "./routes/maintenanceWindowRoute.js";
import settingsRouter from "./routes/settingsRoute.js";
import statusPageRouter from "./routes/statusPageRoute.js";
import { fileURLToPath } from "url";
import queueRouter from "./routes/queueRoute.js";
import AuthRoutes from "./routes/authRoute.js";
import AuthController from "./controllers/authController.js";
import InviteRoutes from "./routes/inviteRoute.js";
import InviteController from "./controllers/inviteController.js";
import MonitorRoutes from "./routes/monitorRoute.js";
import MonitorController from "./controllers/monitorController.js";
import CheckRoutes from "./routes/checkRoute.js";
import CheckController from "./controllers/checkController.js";
import MaintenanceWindowRoutes from "./routes/maintenanceWindowRoute.js";
import MaintenanceWindowController from "./controllers/maintenanceWindowController.js";
import SettingsRoutes from "./routes/settingsRoute.js";
import SettingsController from "./controllers/settingsController.js";
import StatusPageRoutes from "./routes/statusPageRoute.js";
import StatusPageController from "./controllers/statusPageController.js";
import QueueRoutes from "./routes/queueRoute.js";
import QueueController from "./controllers/queueController.js";
//JobQueue service and dependencies
import JobQueue from "./service/jobQueue.js";
@@ -29,6 +44,7 @@ import axios from "axios";
import ping from "ping";
import http from "http";
import Docker from "dockerode";
import net from "net";
// Email service and dependencies
import EmailService from "./service/emailService.js";
@@ -41,102 +57,68 @@ import mjml2html from "mjml";
import SettingsService from "./service/settingsService.js";
import AppSettings from "./db/models/AppSettings.js";
// Status Service and dependencies
import StatusService from "./service/statusService.js";
// Notification Service and dependencies
import NotificationService from "./service/notificationService.js";
import db from "./db/mongo/MongoDB.js";
// Service Registry
import ServiceRegistry from "./service/serviceRegistry.js";
import MongoDB from "./db/mongo/MongoDB.js";
const SERVICE_NAME = "Server";
const SHUTDOWN_TIMEOUT = 1000;
let isShuttingDown = false;
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const openApiSpec = JSON.parse(
fs.readFileSync(path.join(__dirname, "openapi.json"), "utf8")
);
let server;
const PORT = 5000;
const shutdown = async () => {
if (isShuttingDown) {
return;
}
isShuttingDown = true;
logger.info({ message: "Attempting graceful shutdown" });
setTimeout(() => {
logger.error({
message: "Could not shut down in time, forcing shutdown",
service: SERVICE_NAME,
method: "shutdown",
});
process.exit(1);
}, SHUTDOWN_TIMEOUT);
try {
server.close();
await ServiceRegistry.get(JobQueue.SERVICE_NAME).obliterate();
await ServiceRegistry.get(MongoDB.SERVICE_NAME).disconnect();
logger.info({ message: "Graceful shutdown complete" });
process.exit(0);
} catch (error) {
logger.error({
message: error.message,
service: SERVICE_NAME,
method: "shutdown",
stack: error.stack,
});
}
};
// Need to wrap server setup in a function to handle async nature of JobQueue
const startApp = async () => {
const app = express();
// middlewares
app.use(
cors()
//We will add configuration later
);
app.use(express.json());
app.use(helmet());
// Add db, jobQueue, emailService, and settingsService to request object for easy access
app.use((req, res, next) => {
req.db = db;
req.jobQueue = jobQueue;
req.emailService = emailService;
req.settingsService = settingsService;
next();
});
// Swagger UI
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openApiSpec));
//routes
app.use("/api/v1/auth", authRouter);
app.use("/api/v1/settings", verifyJWT, settingsRouter);
app.use("/api/v1/invite", inviteRouter);
app.use("/api/v1/monitors", verifyJWT, monitorRouter);
app.use("/api/v1/checks", verifyJWT, checkRouter);
app.use("/api/v1/maintenance-window", verifyJWT, maintenanceWindowRouter);
app.use("/api/v1/queue", verifyJWT, queueRouter);
app.use("/api/v1/status-page", statusPageRouter);
app.use("/api/v1/dummy-data", async (req, res) => {
try {
const response = await axios.get(
"https://gist.githubusercontent.com/ajhollid/9afa39410c7bbf52cc905f285a2225bf/raw/429a231a3559ebc95f6f488ed2c766bd7d6f46e5/dummyData.json",
{
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache",
},
}
);
return res.status(200).json(response.data);
} catch (error) {
return res.status(500).json({ message: error.message });
}
});
//health check
app.use("/api/v1/healthy", (req, res) => {
try {
logger.info({ message: "Checking Health of the server." });
return res.status(200).json({ message: "Healthy" });
} catch (error) {
logger.error({
message: error.message,
service: SERVICE_NAME,
method: "healthCheck",
stack: error.stack,
});
return res.status(500).json({ message: error.message });
}
});
/**
* Error handler middleware
* Should be called last
*/
app.use(handleErrors);
// Create DB
const db = new MongoDB();
await db.connect();
// Create services
await db.connect();
const server = app.listen(PORT, () => {
logger.info({ message: `server started on port:${PORT}` });
});
const settingsService = new SettingsService(AppSettings);
await settingsService.loadSettings();
const emailService = new EmailService(
@@ -148,7 +130,7 @@ const startApp = async () => {
nodemailer,
logger
);
const networkService = new NetworkService(axios, ping, logger, http, Docker);
const networkService = new NetworkService(axios, ping, logger, http, Docker, net);
const statusService = new StatusService(db, logger);
const notificationService = new NotificationService(emailService, db, logger);
const jobQueue = await JobQueue.createJobQueue(
@@ -162,39 +144,94 @@ const startApp = async () => {
Worker
);
const shutdown = async () => {
if (isShuttingDown) {
return;
}
isShuttingDown = true;
logger.info({ message: "Attempting graceful shutdown" });
setTimeout(() => {
logger.error({
message: "Could not shut down in time, forcing shutdown",
service: SERVICE_NAME,
method: "shutdown",
});
process.exit(1);
}, SHUTDOWN_TIMEOUT);
try {
server.close();
await jobQueue.obliterate();
await db.disconnect();
logger.info({ message: "Graceful shutdown complete" });
process.exit(0);
} catch (error) {
logger.error({
message: error.message,
service: SERVICE_NAME,
method: "shutdown",
stack: error.stack,
});
}
};
// Register services
ServiceRegistry.register(JobQueue.SERVICE_NAME, jobQueue);
ServiceRegistry.register(MongoDB.SERVICE_NAME, db);
ServiceRegistry.register(SettingsService.SERVICE_NAME, settingsService);
ServiceRegistry.register(EmailService.SERVICE_NAME, emailService);
ServiceRegistry.register(NetworkService.SERVICE_NAME, networkService);
ServiceRegistry.register(StatusService.SERVICE_NAME, statusService);
ServiceRegistry.register(NotificationService.SERVICE_NAME, notificationService);
server = app.listen(PORT, () => {
logger.info({ message: `server started on port:${PORT}` });
});
process.on("SIGUSR2", shutdown);
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
//Create controllers
const authController = new AuthController(
ServiceRegistry.get(MongoDB.SERVICE_NAME),
ServiceRegistry.get(SettingsService.SERVICE_NAME),
ServiceRegistry.get(EmailService.SERVICE_NAME),
ServiceRegistry.get(JobQueue.SERVICE_NAME)
);
const monitorController = new MonitorController(
ServiceRegistry.get(MongoDB.SERVICE_NAME),
ServiceRegistry.get(SettingsService.SERVICE_NAME),
ServiceRegistry.get(JobQueue.SERVICE_NAME)
);
const settingsController = new SettingsController(
ServiceRegistry.get(MongoDB.SERVICE_NAME),
ServiceRegistry.get(SettingsService.SERVICE_NAME)
);
const checkController = new CheckController(
ServiceRegistry.get(MongoDB.SERVICE_NAME),
ServiceRegistry.get(SettingsService.SERVICE_NAME)
);
const inviteController = new InviteController(
ServiceRegistry.get(MongoDB.SERVICE_NAME),
ServiceRegistry.get(SettingsService.SERVICE_NAME),
ServiceRegistry.get(EmailService.SERVICE_NAME)
);
const maintenanceWindowController = new MaintenanceWindowController(
ServiceRegistry.get(MongoDB.SERVICE_NAME),
ServiceRegistry.get(SettingsService.SERVICE_NAME)
);
const queueController = new QueueController(ServiceRegistry.get(JobQueue.SERVICE_NAME));
const statusPageController = new StatusPageController(
ServiceRegistry.get(MongoDB.SERVICE_NAME)
);
//Create routes
const authRoutes = new AuthRoutes(authController);
const monitorRoutes = new MonitorRoutes(monitorController);
const settingsRoutes = new SettingsRoutes(settingsController);
const checkRoutes = new CheckRoutes(checkController);
const inviteRoutes = new InviteRoutes(inviteController);
const maintenanceWindowRoutes = new MaintenanceWindowRoutes(
maintenanceWindowController
);
const queueRoutes = new QueueRoutes(queueController);
const statusPageRoutes = new StatusPageRoutes(statusPageController);
// Middleware
app.use(
cors()
//We will add configuration later
);
app.use(express.json());
app.use(helmet());
// Swagger UI
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openApiSpec));
//routes
app.use("/api/v1/auth", authRoutes.getRouter());
app.use("/api/v1/settings", verifyJWT, settingsRoutes.getRouter());
app.use("/api/v1/invite", inviteRoutes.getRouter());
app.use("/api/v1/monitors", verifyJWT, monitorRoutes.getRouter());
app.use("/api/v1/checks", verifyJWT, checkRoutes.getRouter());
app.use("/api/v1/maintenance-window", verifyJWT, maintenanceWindowRoutes.getRouter());
app.use("/api/v1/queue", verifyJWT, queueRoutes.getRouter());
app.use("/api/v1/status-page", statusPageRoutes.getRouter());
app.use(handleErrors);
};
startApp().catch((error) => {
+6 -1
View File
@@ -1,6 +1,9 @@
import jwt from "jsonwebtoken";
const TOKEN_PREFIX = "Bearer ";
const SERVICE_NAME = "allowedRoles";
import ServiceRegistry from "../service/serviceRegistry.js";
import SettingsService from "../service/settingsService.js";
import { errorMessages } from "../utils/messages.js";
const isAllowed = (allowedRoles) => {
@@ -27,7 +30,9 @@ const isAllowed = (allowedRoles) => {
// Parse the token
try {
const parsedToken = token.slice(TOKEN_PREFIX.length, token.length);
const { jwtSecret } = req.settingsService.getSettings();
const { jwtSecret } = ServiceRegistry.get(
SettingsService.SERVICE_NAME
).getSettings();
var decoded = jwt.verify(parsedToken, jwtSecret);
const userRoles = decoded.role;
+6 -3
View File
@@ -1,6 +1,7 @@
import jwt from "jsonwebtoken";
import { errorMessages } from "../utils/messages.js";
import ServiceRegistry from "../service/serviceRegistry.js";
import SettingsService from "../service/settingsService.js";
const SERVICE_NAME = "verifyJWT";
const TOKEN_PREFIX = "Bearer ";
@@ -34,7 +35,7 @@ const verifyJWT = (req, res, next) => {
const parsedToken = token.slice(TOKEN_PREFIX.length, token.length);
// Verify the token's authenticity
const { jwtSecret } = req.settingsService.getSettings();
const { jwtSecret } = ServiceRegistry.get(SettingsService.SERVICE_NAME).getSettings();
jwt.verify(parsedToken, jwtSecret, (err, decoded) => {
if (err) {
if (err.name === "TokenExpiredError") {
@@ -67,7 +68,9 @@ function handleExpiredJwtToken(req, res, next) {
}
// Verify refresh token
const { refreshTokenSecret } = req.settingsService.getSettings();
const { refreshTokenSecret } = ServiceRegistry.get(
SettingsService.SERVICE_NAME
).getSettings();
jwt.verify(refreshToken, refreshTokenSecret, (refreshErr, refreshDecoded) => {
if (refreshErr) {
// Invalid or expired refresh token, trigger logout
+3 -1
View File
@@ -3,6 +3,8 @@ const logger = require("../utils/logger");
const SERVICE_NAME = "verifyAdmin";
const TOKEN_PREFIX = "Bearer ";
const { errorMessages } = require("../utils/messages");
import ServiceRegistry from "../service/serviceRegistry.js";
import SettingsService from "../service/settingsService.js";
/**
* Verifies the JWT token
* @function
@@ -33,7 +35,7 @@ const verifySuperAdmin = (req, res, next) => {
const parsedToken = token.slice(TOKEN_PREFIX.length, token.length);
// verify admin role is present
const { jwtSecret } = req.settingsService.getSettings();
const { jwtSecret } = ServiceRegistry.get(SettingsService.SERVICE_NAME).getSettings();
jwt.verify(parsedToken, jwtSecret, (err, decoded) => {
if (err) {
+83 -21
View File
@@ -11,7 +11,7 @@
"dependencies": {
"axios": "^1.7.2",
"bcrypt": "5.1.1",
"bullmq": "5.34.4",
"bullmq": "5.34.5",
"cors": "^2.8.5",
"dockerode": "4.0.2",
"dotenv": "^16.4.5",
@@ -179,6 +179,17 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/@eslint/config-array/node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
@@ -196,6 +207,19 @@
}
}
},
"node_modules/@eslint/config-array/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/@eslint/config-array/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -237,6 +261,17 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/@eslint/eslintrc/node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
@@ -266,6 +301,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/@eslint/eslintrc/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -1493,12 +1541,12 @@
}
},
"node_modules/bullmq": {
"version": "5.34.4",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.34.4.tgz",
"integrity": "sha512-FPTN5eqsYO5/Blm6vh/bVJ0eADrVLjTBDjMkPCqZN3xx/5GqHov9NwxM6A6M3m6n2Vg+gNAoN98t7bTij5/UEA==",
"version": "5.34.5",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.34.5.tgz",
"integrity": "sha512-MHho9EOhLCTY3ZF+dd0wHv0VlY2FtpBcopMRsvj0kPra4TAwBFh2pik/s4WbX56cIfCE+VzfHIHy4xvqp3g1+Q==",
"license": "MIT",
"dependencies": {
"cron-parser": "^4.6.0",
"cron-parser": "^4.9.0",
"ioredis": "^5.4.1",
"msgpackr": "^1.11.2",
"node-abort-controller": "^3.1.1",
@@ -2357,22 +2405,6 @@
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -2894,6 +2926,17 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/eslint/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/eslint/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -2978,6 +3021,19 @@
"node": ">=8"
}
},
"node_modules/eslint/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/eslint/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -8051,6 +8107,12 @@
"node": ">=0.10.0"
}
},
"node_modules/wordwrap": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
"license": "MIT"
},
"node_modules/workerpool": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz",
+1 -1
View File
@@ -18,7 +18,7 @@
"dependencies": {
"axios": "^1.7.2",
"bcrypt": "5.1.1",
"bullmq": "5.34.4",
"bullmq": "5.34.5",
"cors": "^2.8.5",
"dockerode": "4.0.2",
"dotenv": "^16.4.5",
+48 -26
View File
@@ -5,34 +5,56 @@ import { isAllowed } from "../middleware/isAllowed.js";
import multer from "multer";
import User from "../db/models/User.js";
const router = Router();
const upload = multer();
import {
registerUser,
loginUser,
refreshAuthToken,
editUser,
requestRecovery,
validateRecovery,
resetPassword,
checkSuperadminExists,
getAllUsers,
deleteUser,
} from "../controllers/authController.js";
class AuthRoutes {
constructor(authController) {
this.router = Router();
this.authController = authController;
this.initRoutes();
}
//Auth routes
router.post("/register", upload.single("profileImage"), registerUser);
router.post("/login", loginUser);
router.post("/refresh", refreshAuthToken);
router.put("/user/:userId", upload.single("profileImage"), verifyJWT, editUser);
router.get("/users/superadmin", checkSuperadminExists);
router.get("/users", verifyJWT, isAllowed(["admin", "superadmin"]), getAllUsers);
router.delete("/user/:userId", verifyJWT, verifyOwnership(User, "userId"), deleteUser);
initRoutes() {
this.router.post(
"/register",
upload.single("profileImage"),
this.authController.registerUser
);
this.router.post("/login", this.authController.loginUser);
this.router.post("/refresh", this.authController.refreshAuthToken);
//Recovery routes
router.post("/recovery/request", requestRecovery);
router.post("/recovery/validate", validateRecovery);
router.post("/recovery/reset/", resetPassword);
this.router.put(
"/user/:userId",
upload.single("profileImage"),
verifyJWT,
this.authController.editUser
);
export default router;
this.router.get("/users/superadmin", this.authController.checkSuperadminExists);
this.router.get(
"/users",
verifyJWT,
isAllowed(["admin", "superadmin"]),
this.authController.getAllUsers
);
this.router.delete(
"/user/:userId",
verifyJWT,
verifyOwnership(User, "userId"),
this.authController.deleteUser
);
// Recovery routes
this.router.post("/recovery/request", this.authController.requestRecovery);
this.router.post("/recovery/validate", this.authController.validateRecovery);
this.router.post("/recovery/reset/", this.authController.resetPassword);
}
getRouter() {
return this.router;
}
}
export default AuthRoutes;
+36 -16
View File
@@ -1,26 +1,46 @@
import { Router } from "express";
import {
createCheck,
getChecks,
deleteChecks,
getTeamChecks,
deleteChecksByTeamId,
updateChecksTTL,
} from "../controllers/checkController.js";
import { verifyOwnership } from "../middleware/verifyOwnership.js";
import { isAllowed } from "../middleware/isAllowed.js";
import Monitor from "../db/models/Monitor.js";
const router = Router();
class CheckRoutes {
constructor(checkController) {
this.router = Router();
this.checkController = checkController;
this.initRoutes();
}
router.get("/:monitorId", getChecks);
router.post("/:monitorId", verifyOwnership(Monitor, "monitorId"), createCheck);
router.delete("/:monitorId", verifyOwnership(Monitor, "monitorId"), deleteChecks);
initRoutes() {
this.router.get("/:monitorId", this.checkController.getChecks);
this.router.post(
"/:monitorId",
verifyOwnership(Monitor, "monitorId"),
this.checkController.createCheck
);
this.router.delete(
"/:monitorId",
verifyOwnership(Monitor, "monitorId"),
this.checkController.deleteChecks
);
router.get("/team/:teamId", getTeamChecks);
this.router.get("/team/:teamId", this.checkController.getTeamChecks);
router.delete("/team/:teamId", isAllowed(["admin", "superadmin"]), deleteChecksByTeamId);
this.router.delete(
"/team/:teamId",
isAllowed(["admin", "superadmin"]),
this.checkController.deleteChecksByTeamId
);
router.put("/team/ttl", isAllowed(["admin", "superadmin"]), updateChecksTTL);
this.router.put(
"/team/ttl",
isAllowed(["admin", "superadmin"]),
this.checkController.updateChecksTTL
);
}
export default router;
getRouter() {
return this.router;
}
}
export default CheckRoutes;
+21 -8
View File
@@ -1,14 +1,27 @@
import { Router } from "express";
import { verifyJWT } from "../middleware/verifyJWT.js";
import { isAllowed } from "../middleware/isAllowed.js";
import {
issueInvitation,
inviteVerifyController,
} from "../controllers/inviteController.js";
const router = Router();
class InviteRoutes {
constructor(inviteController) {
this.router = Router();
this.inviteController = inviteController;
this.initRoutes();
}
router.post("/", isAllowed(["admin", "superadmin"]), verifyJWT, issueInvitation);
router.post("/verify", inviteVerifyController);
initRoutes() {
this.router.post(
"/",
isAllowed(["admin", "superadmin"]),
verifyJWT,
this.inviteController.issueInvitation
);
this.router.post("/verify", this.inviteController.inviteVerifyController);
}
export default router;
getRouter() {
return this.router;
}
}
export default InviteRoutes;
+26 -20
View File
@@ -1,31 +1,37 @@
import { Router } from "express";
import {
createMaintenanceWindows,
getMaintenanceWindowById,
getMaintenanceWindowsByTeamId,
getMaintenanceWindowsByMonitorId,
deleteMaintenanceWindow,
editMaintenanceWindow,
} from "../controllers/maintenanceWindowController.js";
import { verifyOwnership } from "../middleware/verifyOwnership.js";
import Monitor from "../db/models/Monitor.js";
const router = Router();
class MaintenanceWindowRoutes {
constructor(maintenanceWindowController) {
this.router = Router();
this.maintenanceWindowController = maintenanceWindowController;
this.initRoutes();
}
initRoutes() {
this.router.post("/", this.maintenanceWindowController.createMaintenanceWindows);
router.post("/", createMaintenanceWindows);
this.router.get(
"/monitor/:monitorId",
verifyOwnership(Monitor, "monitorId"),
this.maintenanceWindowController.getMaintenanceWindowsByMonitorId
);
router.get(
"/monitor/:monitorId",
verifyOwnership(Monitor, "monitorId"),
getMaintenanceWindowsByMonitorId
);
this.router.get(
"/team/",
this.maintenanceWindowController.getMaintenanceWindowsByTeamId
);
router.get("/team/", getMaintenanceWindowsByTeamId);
this.router.get("/:id", this.maintenanceWindowController.getMaintenanceWindowById);
router.get("/:id", getMaintenanceWindowById);
this.router.put("/:id", this.maintenanceWindowController.editMaintenanceWindow);
router.put("/:id", editMaintenanceWindow);
this.router.delete("/:id", this.maintenanceWindowController.deleteMaintenanceWindow);
}
router.delete("/:id", deleteMaintenanceWindow);
getRouter() {
return this.router;
}
}
export default router;
export default MaintenanceWindowRoutes;
+66 -38
View File
@@ -1,51 +1,79 @@
import { Router } from "express";
import {
getAllMonitors,
getAllMonitorsWithUptimeStats,
getMonitorStatsById,
getMonitorCertificate,
getMonitorById,
getMonitorsAndSummaryByTeamId,
getMonitorsByTeamId,
createMonitor,
checkEndpointResolution,
deleteMonitor,
deleteAllMonitors,
editMonitor,
pauseMonitor,
addDemoMonitors,
} from "../controllers/monitorController.js";
import { isAllowed } from "../middleware/isAllowed.js";
import { fetchMonitorCertificate } from "../controllers/controllerUtils.js";
const router = Router();
class MonitorRoutes {
constructor(monitorController) {
this.router = Router();
this.monitorController = monitorController;
this.initRoutes();
}
router.get("/", getAllMonitors);
router.get("/uptime", getAllMonitorsWithUptimeStats);
router.get("/stats/:monitorId", getMonitorStatsById);
router.get("/certificate/:monitorId", (req, res, next) => {
getMonitorCertificate(req, res, next, fetchMonitorCertificate);
});
router.get("/:monitorId", getMonitorById);
router.get("/team/summary/:teamId", getMonitorsAndSummaryByTeamId);
router.get("/team/:teamId", getMonitorsByTeamId);
initRoutes() {
this.router.get("/", this.monitorController.getAllMonitors);
this.router.get("/uptime", this.monitorController.getAllMonitorsWithUptimeStats);
this.router.get("/stats/:monitorId", this.monitorController.getMonitorStatsById);
this.router.get("/certificate/:monitorId", (req, res, next) => {
this.monitorController.getMonitorCertificate(
req,
res,
next,
fetchMonitorCertificate
);
});
this.router.get("/:monitorId", this.monitorController.getMonitorById);
this.router.get(
"/team/summary/:teamId",
this.monitorController.getMonitorsAndSummaryByTeamId
);
this.router.get("/team/:teamId", this.monitorController.getMonitorsByTeamId);
router.get(
"/resolution/url",
isAllowed(["admin", "superadmin"]),
checkEndpointResolution
);
this.router.get(
"/resolution/url",
isAllowed(["admin", "superadmin"]),
this.monitorController.checkEndpointResolution
);
router.delete("/:monitorId", isAllowed(["admin", "superadmin"]), deleteMonitor);
this.router.delete(
"/:monitorId",
isAllowed(["admin", "superadmin"]),
this.monitorController.deleteMonitor
);
router.post("/", isAllowed(["admin", "superadmin"]), createMonitor);
this.router.post(
"/",
isAllowed(["admin", "superadmin"]),
this.monitorController.createMonitor
);
router.put("/:monitorId", isAllowed(["admin", "superadmin"]), editMonitor);
this.router.put(
"/:monitorId",
isAllowed(["admin", "superadmin"]),
this.monitorController.editMonitor
);
router.delete("/", isAllowed(["superadmin"]), deleteAllMonitors);
this.router.delete(
"/",
isAllowed(["superadmin"]),
this.monitorController.deleteAllMonitors
);
router.post("/pause/:monitorId", isAllowed(["admin", "superadmin"]), pauseMonitor);
this.router.post(
"/pause/:monitorId",
isAllowed(["admin", "superadmin"]),
this.monitorController.pauseMonitor
);
router.post("/demo", isAllowed(["admin", "superadmin"]), addDemoMonitors);
this.router.post(
"/demo",
isAllowed(["admin", "superadmin"]),
this.monitorController.addDemoMonitors
);
}
export default router;
getRouter() {
return this.router;
}
}
export default MonitorRoutes;
+17 -17
View File
@@ -1,21 +1,21 @@
import { Router } from "express";
import {
getMetrics,
getJobs,
addJob,
obliterateQueue,
} from "../controllers/queueController.js";
const router = Router();
router.get("/metrics", getMetrics);
class QueueRoutes {
constructor(queueController) {
this.router = Router();
this.queueController = queueController;
this.initRoutes();
}
initRoutes() {
this.router.get("/metrics", this.queueController.getMetrics);
this.router.get("/jobs", this.queueController.getJobs);
this.router.post("/jobs", this.queueController.addJob);
this.router.post("/obliterate", this.queueController.obliterateQueue);
}
// Get Jobs
router.get("/jobs", getJobs);
getRouter() {
return this.router;
}
}
// Add Job
router.post("/jobs", addJob);
// Obliterate Queue
router.post("/obliterate", obliterateQueue);
export default router;
export default QueueRoutes;
+20 -5
View File
@@ -1,10 +1,25 @@
import { Router } from "express";
import { getAppSettings, updateAppSettings } from "../controllers/settingsController.js";
import { isAllowed } from "../middleware/isAllowed.js";
const router = Router();
class SettingsRoutes {
constructor(settingsController) {
this.router = Router();
this.settingsController = settingsController;
this.initRoutes();
}
router.get("/", getAppSettings);
router.put("/", isAllowed(["superadmin"]), updateAppSettings);
initRoutes() {
this.router.get("/", this.settingsController.getAppSettings);
this.router.put(
"/",
isAllowed(["superadmin"]),
this.settingsController.updateAppSettings
);
}
export default router;
getRouter() {
return this.router;
}
}
export default SettingsRoutes;
+21 -10
View File
@@ -1,11 +1,22 @@
import express from "express";
import { verifyJWT } from "../middleware/verifyJWT.js";
import {
createStatusPage,
getStatusPageByUrl,
} from "../controllers/statusPageController.js";
const router = express.Router();
import { Router } from "express";
router.get("/:url", getStatusPageByUrl);
router.post("/:url", verifyJWT, createStatusPage);
export default router;
import { verifyJWT } from "../middleware/verifyJWT.js";
class StatusPageRoutes {
constructor(statusPageController) {
this.router = Router();
this.statusPageController = statusPageController;
this.initRoutes();
}
initRoutes() {
this.router.get("/:url", this.statusPageController.getStatusPageByUrl);
this.router.post("/:url", verifyJWT, this.statusPageController.createStatusPage);
}
getRouter() {
return this.router;
}
}
export default StatusPageRoutes;
+1
View File
@@ -10,6 +10,7 @@ const SERVICE_NAME = "EmailService";
* Represents an email service that can load templates, build, and send emails.
*/
class EmailService {
static SERVICE_NAME = SERVICE_NAME;
/**
* Constructs an instance of the EmailService, initializing template loaders and the email transporter.
* @param {Object} settingsService - The settings service to get email configuration.
+25 -4
View File
@@ -1,7 +1,8 @@
const QUEUE_NAME = "monitors";
const JOBS_PER_WORKER = 5;
import { errorMessages, successMessages } from "../utils/messages.js";
const SERVICE_NAME = "JobQueue";
import { errorMessages, successMessages } from "../utils/messages.js";
/**
* JobQueue
*
@@ -10,6 +11,7 @@ const SERVICE_NAME = "JobQueue";
* It scales the number of workers based on the number of jobs/worker
*/
class JobQueue {
static SERVICE_NAME = SERVICE_NAME;
/**
* @class JobQueue
* @classdesc Manages job queue and workers.
@@ -88,11 +90,30 @@ class JobQueue {
const monitors = await db.getAllMonitors();
for (const monitor of monitors) {
if (monitor.isActive) {
await queue.addJob(monitor.id, monitor);
queue.addJob(monitor.id, monitor).catch((error) => {
this.logger.error({
message: error.message,
service: SERVICE_NAME,
method: "createJobQueue",
stack: error.stack,
});
});
}
}
const workerStats = await queue.getWorkerStats();
await queue.scaleWorkers(workerStats);
queue
.getWorkerStats()
.then((workerStats) => {
queue.scaleWorkers(workerStats);
})
.catch((error) => {
this.logger.error({
message: error.message,
service: SERVICE_NAME,
method: "createJobQueue",
stack: error.stack,
});
});
return queue;
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
+65 -2
View File
@@ -1,4 +1,6 @@
import { errorMessages, successMessages } from "../utils/messages.js";
const SERVICE_NAME = "NetworkService";
/**
* Constructs a new NetworkService instance.
*
@@ -6,15 +8,18 @@ import { errorMessages, successMessages } from "../utils/messages.js";
* @param {Object} ping - The ping utility for network checks.
* @param {Object} logger - The logger instance for logging.
* @param {Object} http - The HTTP utility for network operations.
* @param {Object} net - The net utility for network operations.
*/
class NetworkService {
constructor(axios, ping, logger, http, Docker) {
static SERVICE_NAME = SERVICE_NAME;
constructor(axios, ping, logger, http, Docker, net) {
this.TYPE_PING = "ping";
this.TYPE_HTTP = "http";
this.TYPE_PAGESPEED = "pagespeed";
this.TYPE_HARDWARE = "hardware";
this.TYPE_DOCKER = "docker";
this.SERVICE_NAME = "NetworkService";
this.TYPE_PORT = "port";
this.SERVICE_NAME = SERVICE_NAME;
this.NETWORK_ERROR = 5000;
this.PING_ERROR = 5001;
this.axios = axios;
@@ -22,6 +27,7 @@ class NetworkService {
this.logger = logger;
this.http = http;
this.Docker = Docker;
this.net = net;
}
/**
@@ -262,6 +268,60 @@ class NetworkService {
}
}
async requestPort(job) {
try {
const { url, port } = job.data;
const { response, responseTime, error } = await this.timeRequest(async () => {
return new Promise((resolve, reject) => {
const socket = this.net.createConnection(
{
host: url,
port,
},
() => {
socket.end();
socket.destroy();
resolve({ success: true });
}
);
socket.setTimeout(5000);
socket.on("timeout", () => {
socket.destroy();
reject(new Error("Connection timeout"));
});
socket.on("error", (err) => {
socket.destroy();
reject(err);
});
});
});
const portResponse = {
monitorId: job.data._id,
type: job.data.type,
responseTime,
};
if (error) {
portResponse.status = false;
portResponse.code = this.NETWORK_ERROR;
portResponse.message = errorMessages.PORT_FAIL;
return portResponse;
}
portResponse.status = response.success;
portResponse.code = 200;
portResponse.message = successMessages.PORT_SUCCESS;
return portResponse;
} catch (error) {
error.service = this.SERVICE_NAME;
error.method = "requestTCP";
throw error;
}
}
/**
* Handles unsupported job types by throwing an error with details.
*
@@ -297,6 +357,9 @@ class NetworkService {
return await this.requestHardware(job);
case this.TYPE_DOCKER:
return await this.requestDocker(job);
case this.TYPE_PORT:
return await this.requestPort(job);
default:
return this.handleUnsupportedType(type);
}
+4 -1
View File
@@ -1,4 +1,7 @@
const SERVICE_NAME = "NotificationService";
class NotificationService {
static SERVICE_NAME = SERVICE_NAME;
/**
* Creates an instance of NotificationService.
*
@@ -7,7 +10,7 @@ class NotificationService {
* @param {Object} logger - The logger instance for logging activities.
*/
constructor(emailService, db, logger) {
this.SERVICE_NAME = "NotificationService";
this.SERVICE_NAME = SERVICE_NAME;
this.emailService = emailService;
this.db = db;
this.logger = logger;
+35
View File
@@ -0,0 +1,35 @@
const SERVICE_NAME = "ServiceRegistry";
import logger from "../utils/logger.js";
class ServiceRegistry {
static SERVICE_NAME = SERVICE_NAME;
constructor() {
this.services = {};
}
register(name, service) {
logger.info({
message: `Registering service ${name}`,
service: SERVICE_NAME,
method: "register",
});
this.services[name] = service;
}
get(name) {
if (!this.services[name]) {
logger.error({
message: `Service ${name} is not registered`,
service: SERVICE_NAME,
method: "get",
});
throw new Error(`Service ${name} is not registered`);
}
return this.services[name];
}
listServices() {
return Object.keys(this.services);
}
}
export default new ServiceRegistry();
+1
View File
@@ -25,6 +25,7 @@ const envConfig = {
* from the database if they are not set in the environment.
*/
class SettingsService {
static SERVICE_NAME = SERVICE_NAME;
/**
* Constructs a new SettingsService
* @constructor
+5 -1
View File
@@ -1,4 +1,7 @@
const SERVICE_NAME = "StatusService";
class StatusService {
static SERVICE_NAME = SERVICE_NAME;
/**
* Creates an instance of StatusService.
*
@@ -8,7 +11,7 @@ class StatusService {
constructor(db, logger) {
this.db = db;
this.logger = logger;
this.SERVICE_NAME = "StatusService";
this.SERVICE_NAME = SERVICE_NAME;
}
getStatusString = (status) => {
@@ -140,6 +143,7 @@ class StatusService {
pagespeed: this.db.createPageSpeedCheck,
hardware: this.db.createHardwareCheck,
docker: this.db.createCheck,
port: this.db.createCheck,
};
const operation = operationMap[networkResponse.type];
+6
View File
@@ -59,6 +59,9 @@ const errorMessages = {
// Docker
DOCKER_FAIL: "Failed to fetch Docker container information",
DOCKER_NOT_FOUND: "Docker container not found",
// Port
PORT_FAIL: "Failed to connect to port",
};
const successMessages = {
@@ -129,6 +132,9 @@ const successMessages = {
// Docker
DOCKER_SUCCESS: "Docker container status fetched successfully",
// Port
PORT_SUCCESS: "Port connected successfully",
};
export { errorMessages, successMessages };
+9 -4
View File
@@ -144,10 +144,12 @@ const getMonitorsAndSummaryByTeamIdQueryValidation = joi.object({
type: joi
.alternatives()
.try(
joi.string().valid("http", "ping", "pagespeed", "docker", "hardware"),
joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port"),
joi
.array()
.items(joi.string().valid("http", "ping", "pagespeed", "docker", "hardware"))
.items(
joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port")
)
),
});
@@ -163,10 +165,12 @@ const getMonitorsByTeamIdQueryValidation = joi.object({
type: joi
.alternatives()
.try(
joi.string().valid("http", "ping", "pagespeed", "docker", "hardware"),
joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port"),
joi
.array()
.items(joi.string().valid("http", "ping", "pagespeed", "docker", "hardware"))
.items(
joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port")
)
),
page: joi.number(),
rowsPerPage: joi.number(),
@@ -199,6 +203,7 @@ const createMonitorBodyValidation = joi.object({
description: joi.string().required(),
type: joi.string().required(),
url: joi.string().required(),
port: joi.number(),
isActive: joi.boolean(),
interval: joi.number(),
thresholds: joi.object().keys({