From ebf6a0ceb718527a8bcb412d20a189d9e2ac745a Mon Sep 17 00:00:00 2001 From: om-3004 Date: Tue, 15 Oct 2024 17:20:54 +0530 Subject: [PATCH] Check the monitor URL resolution before adding it --- .../UptimeMonitors/uptimeMonitorsSlice.js | 42 ++++++++ .../Pages/Monitors/CreateMonitor/index.jsx | 98 +++++++++---------- Client/src/Utils/NetworkService.js | 26 +++++ Server/controllers/monitorController.js | 39 ++++++++ Server/routes/monitorRoute.js | 6 ++ 5 files changed, 161 insertions(+), 50 deletions(-) diff --git a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js index f6c3cb10a..05ec9b316 100644 --- a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js +++ b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js @@ -31,6 +31,30 @@ export const createUptimeMonitor = createAsyncThunk( } ); +export const checkEndpointResolution = createAsyncThunk( + "monitors/checkEnpoint", + async(data, thunkApi) => { + try{ + const { authToken, monitorURL } = data; + + const res = await networkService.checkEndpointResolution({ + authToken: authToken, + monitorURL: monitorURL, + }) + return res.data; + } catch (error) { + if (error.response && error.response.data) { + return thunkApi.rejectWithValue(error.response.data); + } + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); + } + } +) + export const getUptimeMonitorById = createAsyncThunk( "monitors/getMonitorById", async (data, thunkApi) => { @@ -271,6 +295,24 @@ const uptimeMonitorsSlice = createSlice({ : "Failed to create uptime monitor"; }) // ***************************************************** + // Resolve Endpoint + // ***************************************************** + .addCase(checkEndpointResolution.pending, (state) => { + state.isLoading = true; + }) + .addCase(checkEndpointResolution.fulfilled, (state, action) => { + state.isLoading = false; + state.success = action.payload.success; + state.msg = action.payload.msg; + }) + .addCase(checkEndpointResolution.rejected, (state, action) => { + state.isLoading = false; + state.success = false; + state.msg = action.payload + ? action.payload.msg + : "Failed to check endpoint resolution"; + }) + // ***************************************************** // Get Monitor By Id // ***************************************************** .addCase(getUptimeMonitorById.pending, (state) => { diff --git a/Client/src/Pages/Monitors/CreateMonitor/index.jsx b/Client/src/Pages/Monitors/CreateMonitor/index.jsx index ac6e1258c..50d4e3195 100644 --- a/Client/src/Pages/Monitors/CreateMonitor/index.jsx +++ b/Client/src/Pages/Monitors/CreateMonitor/index.jsx @@ -3,6 +3,7 @@ import { Box, Button, ButtonGroup, Stack, Typography } from "@mui/material"; import { useSelector, useDispatch } from "react-redux"; import { monitorValidation } from "../../../Validation/validation"; import { createUptimeMonitor } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; +import { checkEndpointResolution } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice" import { useNavigate, useParams } from "react-router-dom"; import { useTheme } from "@emotion/react"; import { createToast } from "../../../Utils/toastUtils"; @@ -124,58 +125,55 @@ const CreateMonitor = () => { const handleCreateMonitor = async (event) => { event.preventDefault(); + //obj to submit + let form = { + url: + //preprending protocol for url + monitor.type === "http" + ? `http${https ? "s" : ""}://` + monitor.url + : monitor.url, + name: monitor.name === "" ? monitor.url : monitor.name, + type: monitor.type, + interval: monitor.interval * MS_PER_MINUTE, + }; - const formattedUrl = monitor.type === "http" - ? `http${https ? "s" : ""}://` + monitor.url - : monitor.url; + const { error } = monitorValidation.validate(form, { + abortEarly: false, + }); - try { - // Check if the URL resolves by sending a request - const response = await fetch(formattedUrl, { method: 'GET', mode: 'no-cors' }); - if (!response) { - throw new Error(); - } - - //obj to submit - let form = { - url:formattedUrl, - name: monitor.name === "" ? monitor.url : monitor.name, - type: monitor.type, - interval: monitor.interval * MS_PER_MINUTE, - }; - - const { error } = monitorValidation.validate(form, { - abortEarly: false, + if (error) { + const newErrors = {}; + error.details.forEach((err) => { + newErrors[err.path[0]] = err.message; }); - - if (error) { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - setErrors(newErrors); - createToast({ body: "Error validation data." }); - } else { - form = { - ...form, - description: form.name, - teamId: user.teamId, - userId: user._id, - notifications: monitor.notifications, - }; - const action = await dispatch( - createUptimeMonitor({ authToken, monitor: form }) - ); - if (action.meta.requestStatus === "fulfilled") { - createToast({ body: "Monitor created successfully!" }); - navigate("/monitors"); - } else { - createToast({ body: "Failed to create monitor." }); - } + setErrors(newErrors); + createToast({ body: "Error validation data." }); + } else { + const checkEndpointAction = await dispatch( + checkEndpointResolution({ authToken, monitorURL: form.url }) + ) + if (checkEndpointAction.meta.requestStatus === "rejected") { + createToast({ body: "The endpoint you entered doesn't resolve. Check the URL again." }); + setErrors({ url: "The entered URL is not reachable." }); + return; + } + + form = { + ...form, + description: form.name, + teamId: user.teamId, + userId: user._id, + notifications: monitor.notifications, + }; + const action = await dispatch( + createUptimeMonitor({ authToken, monitor: form }) + ); + if (action.meta.requestStatus === "fulfilled") { + createToast({ body: "Monitor created successfully!" }); + navigate("/monitors"); + } else { + createToast({ body: "Failed to create monitor." }); } - } catch (error) { - createToast({ body: "The endpoint you entered doesn't resolve. Check the URL again." }); - setErrors({ url: "The entered URL is not reachable." }); } }; @@ -387,7 +385,7 @@ const CreateMonitor = () => { onClick={handleCreateMonitor} disabled={Object.keys(errors).length !== 0 && true} > - {monitor.type === "http" ? "Create Monitor" : "Check Endpoint"} + Create Monitor @@ -395,4 +393,4 @@ const CreateMonitor = () => { ); }; -export default CreateMonitor; +export default CreateMonitor; \ No newline at end of file diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js index 573651f58..a9dd80731 100644 --- a/Client/src/Utils/NetworkService.js +++ b/Client/src/Utils/NetworkService.js @@ -89,6 +89,32 @@ class NetworkService { }); } + /** + * + * ************************************ + * Check the endpoint resolution + * ************************************ + * + * @async + * @param {Object} config - The configuration object. + * @param {string} config.authToken - The authorization token to be used in the request header. + * @param {Object} config.monitorURL - The monitor url to be sent in the request body. + * @returns {Promise} The response from the axios POST request. + */ + async checkEndpointResolution(config) { + const { authToken, monitorURL } = config; + const params = new URLSearchParams(); + + if (monitorURL) params.append("monitorURL", monitorURL); + + return this.axiosInstance.get(`/monitors/check-endpoint/url?${params.toString()}`, { + headers: { + Authorization: `Bearer ${authToken}`, + "Content-Type": "application/json", + } + }) + } + /** * * ************************************ diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index b7e27451b..44ba5a0e3 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -20,6 +20,7 @@ const jwt = require("jsonwebtoken"); const { getTokenFromHeaders } = require("../utils/utils"); const logger = require("../utils/logger"); const { handleError, handleValidationError } = require("./controllerUtils"); +const dns = require('dns'); /** * Returns all monitors @@ -262,6 +263,43 @@ const createMonitor = async (req, res, next) => { } }; +/** + * Checks if the endpoint can be resolved and adds it to the job queue. + * @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 { + let { monitorURL } = req.query; + + // Remove the protocol (http:// or https://) if present + monitorURL = monitorURL.replace(/^https?:\/\//, ''); + // Remove the trailing slash if it exists + monitorURL = monitorURL.endsWith('/') ? monitorURL.slice(0, -1) : monitorURL; + + dns.setServers(['8.8.8.8', '1.1.1.1']);// Google, Cloudfare + dns.resolve(monitorURL, (error) => { + console.log("Inside .resolve"); + if (error) { + next(handleError(error, SERVICE_NAME, "checkEndpointResolution")); + return ; + } + return res.status(200).json({ + success: true, + 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 @@ -475,6 +513,7 @@ module.exports = { getMonitorsAndSummaryByTeamId, getMonitorsByTeamId, createMonitor, + checkEndpointResolution, deleteMonitor, deleteAllMonitors, editMonitor, diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js index aaa9c10e9..c3f695556 100644 --- a/Server/routes/monitorRoute.js +++ b/Server/routes/monitorRoute.js @@ -18,6 +18,12 @@ router.post( monitorController.createMonitor ); +router.get( + "/check-endpoint/url", + isAllowed(["admin", "superadmin"]), + monitorController.checkEndpointResolution +) + router.delete( "/:monitorId", isAllowed(["admin", "superadmin"]),