Merge branch 'bluewave-labs:master' into monitors

This commit is contained in:
Mohammad Khalilzadeh
2024-06-18 22:42:21 +03:30
committed by GitHub
9 changed files with 292 additions and 69 deletions

View File

@@ -1,19 +1,32 @@
import PropTypes from "prop-types";
import ComplexAlert from "../../Components/Icons/ComplexAlert/ComplexAlert";
import AnnouncementsDualButtonWithIcon from "../../Components/Announcements/AnnouncementsDualButtonWithIcon/AnnouncementsDualButtonWithIcon";
import "react-toastify/dist/ReactToastify.css";
import { toast, ToastContainer } from "react-toastify";
import "./index.css";
const ToastComponent = () => {
/**
* ToastComponent displays a notification that is triggered by error messages.
*
* @component
* @param {Object} props - The component props
* @param {string} props.subject - The subject or title of the toast message
* @param {string} props.body - The body content of the toast message
* @param {string} props.esc - The text for the dismiss action button
* @param {string} props.primary - The text for the primary action button
* @returns {JSX.Element} JSX element representing the ToastComponent
*/
const ToastComponent = ({ subject, body, esc, primary }) => {
const displayMsg = () => {
toast(
({ closeToast, toastProps }) => (
<AnnouncementsDualButtonWithIcon
icon={<ComplexAlert theme="red" />}
subject="There was a problem with that action"
body="Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquid pariatur, ipsum dolor."
esc="Dismiss"
primary="Learn more"
subject={subject}
body={body}
esc={esc}
primary={primary}
closeToast={closeToast}
/>
),
@@ -29,4 +42,11 @@ const ToastComponent = () => {
);
};
ToastComponent.propTypes = {
subject: PropTypes.string.isRequired,
body: PropTypes.string.isRequired,
esc: PropTypes.string.isRequired,
primary: PropTypes.string.isRequired,
};
export default ToastComponent;

View File

@@ -161,8 +161,10 @@ const deleteMonitor = async (req, res, next) => {
try {
const monitor = await req.db.deleteMonitor(req, res, next);
req.jobQueue.deleteJob(monitor);
// Delete associated checks and alerts
await req.jobQueue.deleteJob(monitor);
await req.db.deleteChecks(monitor._id);
await req.db.deleteAlertByMonitorId(monitor._id);
/**
* TODO
* We should remove all checks and alerts associated with this monitor
@@ -178,6 +180,18 @@ const deleteMonitor = async (req, res, next) => {
}
};
const deleteAllMonitors = async (req, res) => {
try {
const deleteCount = await req.db.deleteAllMonitors();
return res
.status(200)
.json({ success: true, msg: `Deleted ${deleteCount} monitors` });
} catch (error) {
error.service = SERVICE_NAME;
next(error);
}
};
/**
* Edit a monitor by ID
* @async
@@ -200,13 +214,11 @@ const editMonitor = async (req, res, next) => {
try {
const editedMonitor = await req.db.editMonitor(req, res);
return res
.status(200)
.json({
success: true,
msg: successMessages.MONITOR_EDIT,
data: editedMonitor,
});
return res.status(200).json({
success: true,
msg: successMessages.MONITOR_EDIT,
data: editedMonitor,
});
} catch (error) {
error.service = SERVICE_NAME;
next(error);
@@ -219,5 +231,6 @@ module.exports = {
getMonitorsByUserId,
createMonitor,
deleteMonitor,
deleteAllMonitors,
editMonitor,
};

View File

@@ -258,6 +258,19 @@ const deleteMonitor = async (req, res) => {
}
};
/**
* DELETE ALL MONITORS (TEMP)
*/
const deleteAllMonitors = async (req, res) => {
try {
const deletedCount = await Monitor.deleteMany({});
return deletedCount.deletedCount;
} catch (error) {
throw error;
}
};
/**
* Edit a monitor by ID
* @async
@@ -336,11 +349,7 @@ const getChecks = async (monitorId) => {
const deleteChecks = async (monitorId) => {
try {
const result = await Check.deleteMany({ monitorId });
if (result.deletedCount > 0) {
return result.deletedCount;
} else {
throw new Error(errorMessages.DB_DELETE_CHECKS(monitorId));
}
return result.deletedCount;
} catch (error) {
throw error;
}
@@ -457,11 +466,16 @@ const editAlert = async (alertId, alertData) => {
const deleteAlert = async (alertId) => {
try {
const result = await Alert.findByIdAndDelete(alertId);
if (result) {
return result;
} else {
throw new Error(errorMessages.DB_DELETE_ALERT(alertId));
}
return result;
} catch (error) {
throw error;
}
};
const deleteAlertByMonitorId = async (monitorId) => {
try {
const result = await Alert.deleteMany({ monitorId });
return result;
} catch (error) {
throw error;
}
@@ -480,6 +494,7 @@ module.exports = {
getMonitorsByUserId,
createMonitor,
deleteMonitor,
deleteAllMonitors,
editMonitor,
createCheck,
getChecks,
@@ -490,4 +505,5 @@ module.exports = {
getAlertById,
editAlert,
deleteAlert,
deleteAlertByMonitorId,
};

View File

@@ -15,34 +15,36 @@ const verifyJWT = (req, res, next) => {
const token = req.headers["authorization"];
// Make sure a token is provided
if (!token) {
const error = new Error(error.errorMessages.NO_AUTH_TOKEN);
const error = new Error(errorMessages.NO_AUTH_TOKEN);
error.status = 401;
error.service = SERVICE_NAME;
next(error);
return;
}
// Make sure it is properly formatted
if (token.startsWith(TOKEN_PREFIX)) {
const parsedToken = token.slice(TOKEN_PREFIX.length, token.length);
// Verify the token's authenticity
jwt.verify(parsedToken, process.env.JWT_SECRET, (err, decoded) => {
if (err) {
logger.error(errorMessages.INVALID_AUTH_TOKEN, {
service: SERVICE_NAME,
});
return res
.status(401)
.json({ success: false, msg: errorMessages.INVALID_AUTH_TOKEN });
}
//Add the user to the request object for use in the route
req.user = decoded;
next();
});
} else {
if (!token.startsWith(TOKEN_PREFIX)) {
const error = new Error(errorMessages.INVALID_AUTH_TOKEN); // Instantiate a new Error object for improperly formatted token
error.status = 400;
error.service = SERVICE_NAME;
next(error);
return;
}
const parsedToken = token.slice(TOKEN_PREFIX.length, token.length);
// Verify the token's authenticity
jwt.verify(parsedToken, process.env.JWT_SECRET, (err, decoded) => {
if (err) {
logger.error(errorMessages.INVALID_AUTH_TOKEN, {
service: SERVICE_NAME,
});
return res
.status(401)
.json({ success: false, msg: errorMessages.INVALID_AUTH_TOKEN });
}
//Add the user to the request object for use in the route
req.user = decoded;
next();
});
};
module.exports = { verifyJWT };

View File

@@ -20,7 +20,7 @@ const verifyOwnership = (Model, paramName) => {
// If the userID does not match the document's userID, return a 403 error
if (userId.toString() !== doc.userId.toString()) {
console.log("boom");
console.log(userId.toString(), doc.userId.toString());
const error = new Error(errorMessages.VERIFY_OWNER_UNAUTHORIZED);
error.status = 403;
throw error;

View File

@@ -2,25 +2,42 @@ const router = require("express").Router();
const alertController = require("../controllers/alertController");
const { verifyOwnership } = require("../middleware/verifyOwnership");
const Alert = require("../models/Alert");
const Monitor = require("../models/Monitor");
// Create alert
router.post("/:monitorId", alertController.createAlert);
router.post(
"/:monitorId",
verifyOwnership(Monitor, "monitorId"),
alertController.createAlert
);
// Get all alerts for a user
router.get("/user/:userId", alertController.getAlertsByUserId);
router.get(
"/user/:userId",
verifyOwnership(Monitor, "monitorId"),
alertController.getAlertsByUserId
);
// Get all alerts for a monitor
router.get("/monitor/:monitorId", alertController.getAlertsByMonitorId);
router.get(
"/monitor/:monitorId",
verifyOwnership(Monitor, "monitorId"),
alertController.getAlertsByMonitorId
);
// Get a single alert
router.get("/:alertId", alertController.getAlertById);
router.get(
"/:alertId",
verifyOwnership(Monitor, "monitorId"),
alertController.getAlertById
);
// Edit
router.post(
"/edit/:alertId",
verifyOwnership(Alert, "alertId"),
verifyOwnership(Monitor, "monitorId"),
alertController.editAlert
);
//Delete
router.post(
"/delete/:alertId",
verifyOwnership(Alert, "alertId"),
verifyOwnership(Monitor, "monitorId"),
alertController.deleteAlert
);

View File

@@ -18,4 +18,6 @@ router.post(
verifyOwnership(Monitor, "monitorId"),
monitorController.editMonitor
);
router.delete("/delete/all", monitorController.deleteAllMonitors);
module.exports = router;

View File

@@ -7,9 +7,8 @@ const connection = {
const JOBS_PER_WORKER = 5;
const logger = require("../utils/logger");
const { errorMessages, successMessages } = require("../utils/messages");
const NetworkService = require("./networkService");
const SERVICE_NAME = "JobQueue";
const axios = require("axios");
const ping = require("ping");
class JobQueue {
/**
@@ -23,6 +22,7 @@ class JobQueue {
});
this.workers = [];
this.db = null;
this.networkService = null;
}
/**
@@ -36,6 +36,7 @@ class JobQueue {
const queue = new JobQueue();
try {
queue.db = db;
queue.networkService = new NetworkService(db);
const monitors = await db.getAllMonitors();
for (const monitor of monitors) {
await queue.addJob(monitor.id, monitor);
@@ -57,24 +58,14 @@ class JobQueue {
const worker = new Worker(
QUEUE_NAME,
async (job) => {
// TODO Extract to service
if (job.data.type === "ping") {
const response = await ping.promise.probe(job.data.url);
// TODO insert checks into DB
if (response.alive === true) {
console.log(`${job.data.url} is alive`);
} else {
console.log(`${job.data.url} is dead`);
}
} else if (job.data.type === "http") {
const response = await axios.get(job.data.url);
// TODO create a check object and save it to the db
if (response.status >= 200 && response.status < 300) {
// TODO insert checks into DB
console.log(`${job.data.url} is alive`);
} else {
console.log(`${job.data.url} is dead`);
}
try {
const res = await this.networkService.getStatus(job);
} catch (error) {
logger.error(`Error processing job ${job.id}: ${error.message}`, {
service: SERVICE_NAME,
jobId: job.id,
error: error,
});
}
},
{

View File

@@ -0,0 +1,162 @@
const axios = require("axios");
const ping = require("ping");
const logger = require("../utils/logger");
const Check = require("../models/Check");
class NetworkService {
constructor(db) {
this.db = db;
this.TYPE_PING = "ping";
this.TYPE_HTTP = "http";
this.SERVICE_NAME = "NetworkService";
this.NETWORK_ERROR = 5000;
}
/**
* Measures the response time of an asynchronous operation.
* @param {Function} operation - An asynchronous operation to measure.
* @returns {Promise<{responseTime: number, response: any}>} An object containing the response time in milliseconds and the response from the operation.
* @throws {Error} The error object from the operation, contains response time.
*/
async measureResponseTime(operation) {
const startTime = Date.now();
try {
const response = await operation();
const endTime = Date.now();
return { responseTime: endTime - startTime, response };
} catch (error) {
const endTime = Date.now();
error.responseTime = endTime - startTime;
throw error;
}
}
/**
* Handles the ping operation for a given job, measures its response time, and logs the result.
* @param {Object} job - The job object containing data for the ping operation.
* @returns {Promise<{boolean}} The result of logging and storing the check
*/
async handlePing(job) {
const operation = async () => {
const response = await ping.promise.probe(job.data.url);
return response;
};
try {
const { responseTime, response } = await this.measureResponseTime(
operation
);
const isAlive = response.alive;
const check = new Check({
monitorId: job.data._id,
status: isAlive,
responseTime,
});
return await this.logAndStoreCheck(check);
} catch (error) {
const check = new Check({
monitorId: job.data._id,
status: false,
responseTime: error.responseTime,
});
return await this.logAndStoreCheck(check);
}
}
/**
* Handles the http operation for a given job, measures its response time, and logs the result.
* @param {Object} job - The job object containing data for the ping operation.
* @returns {Promise<{boolean}} The result of logging and storing the check
*/
async handleHttp(job) {
// Define operation for timing
const operation = async () => {
const response = await axios.get(job.data.url);
return response;
};
// attempt connection
try {
const { responseTime, response } = await this.measureResponseTime(
operation
);
// check if response is in the 200 range, if so, service is up
const isAlive = response.status >= 200 && response.status < 300;
//Create a check with relevant data
const check = new Check({
monitorId: job.data._id,
status: isAlive,
responseTime,
statusCode: response.status,
});
return await this.logAndStoreCheck(check);
} catch (error) {
const check = new Check({
monitorId: job.data._id,
status: false,
responseTime: error.responseTime,
});
// The server returned a response
if (error.response) {
check.statusCode = error.response.status;
} else {
check.statusCode = this.NETWORK_ERROR;
}
return await this.logAndStoreCheck(check);
}
}
/**
* Retrieves the status of a given job based on its type.
* For unsupported job types, it logs an error and returns false.
*
* @param {Object} job - The job object containing data necessary for processing.
* @returns {Promise<boolean>} The status of the job if it is supported and processed successfully, otherwise false.
*/
async getStatus(job) {
switch (job.data.type) {
case this.TYPE_PING:
return await this.handlePing(job);
case this.TYPE_HTTP:
return await this.handleHttp(job);
default:
logger.error(`Unsupported type: ${job.data.type}`, {
service: this.SERVICE_NAME,
jobId: job.id,
});
return false;
}
}
/**
* Logs and stores the result of a check for a specific job.
* This function creates a new Check object with the job's details and the result of the check,
* then attempts to save this object to the database. If the save operation is successful,
* it returns the status of the inserted check. If an error occurs during the save operation,
* it logs the error and returns false.
*
* @param {Object} job - The job object containing data necessary for the check.
* @param {boolean} isAlive - The result of the check, indicating if the target is alive.
* @param {number} responseTime - The response time measured during the check.
* @param {Error} [error=null] - Optional error object if an error occurred during the check.
* @returns {Promise<boolean>} The status of the inserted check if successful, otherwise false.
*/
async logAndStoreCheck(check) {
try {
const insertedCheck = await check.save();
return insertedCheck.status;
} catch (error) {
logger.error(`Error wrtiting check for ${job.id}`, {
service: this.SERVICE_NAME,
jobId: job.id,
error: error,
});
return false;
}
}
}
module.exports = NetworkService;