diff --git a/Client/src/Components/Toast/index.jsx b/Client/src/Components/Toast/index.jsx index ceb7a6f60..87e6c2ea9 100644 --- a/Client/src/Components/Toast/index.jsx +++ b/Client/src/Components/Toast/index.jsx @@ -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 }) => ( } - 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; diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index fb1a0fc78..800ec994c 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -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, }; diff --git a/Server/db/MongoDB.js b/Server/db/MongoDB.js index 4f1d0c5a7..40db3c5e2 100644 --- a/Server/db/MongoDB.js +++ b/Server/db/MongoDB.js @@ -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, }; diff --git a/Server/middleware/verifyJWT.js b/Server/middleware/verifyJWT.js index 9c9a3759a..1dbbe8d99 100644 --- a/Server/middleware/verifyJWT.js +++ b/Server/middleware/verifyJWT.js @@ -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 }; diff --git a/Server/middleware/verifyOwnership.js b/Server/middleware/verifyOwnership.js index cc7f544f7..950d0a6e4 100644 --- a/Server/middleware/verifyOwnership.js +++ b/Server/middleware/verifyOwnership.js @@ -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; diff --git a/Server/routes/alertRoute.js b/Server/routes/alertRoute.js index be252e8c4..ba30fd117 100644 --- a/Server/routes/alertRoute.js +++ b/Server/routes/alertRoute.js @@ -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 ); diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js index ffdce1d4d..ae7a5852e 100644 --- a/Server/routes/monitorRoute.js +++ b/Server/routes/monitorRoute.js @@ -18,4 +18,6 @@ router.post( verifyOwnership(Monitor, "monitorId"), monitorController.editMonitor ); + +router.delete("/delete/all", monitorController.deleteAllMonitors); module.exports = router; diff --git a/Server/service/jobQueue.js b/Server/service/jobQueue.js index 43983bc48..ae735ae86 100644 --- a/Server/service/jobQueue.js +++ b/Server/service/jobQueue.js @@ -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, + }); } }, { diff --git a/Server/service/networkService.js b/Server/service/networkService.js new file mode 100644 index 000000000..f3f40999d --- /dev/null +++ b/Server/service/networkService.js @@ -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} 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} 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;