From 4a1b290416c020fbc9b4b1b927c877e0645f21bb Mon Sep 17 00:00:00 2001 From: om-3004 Date: Sat, 12 Oct 2024 22:58:39 +0530 Subject: [PATCH 01/21] check if the url resolves before adding the monitor --- .../Pages/Monitors/CreateMonitor/index.jsx | 88 +++++++++++-------- 1 file changed, 50 insertions(+), 38 deletions(-) diff --git a/Client/src/Pages/Monitors/CreateMonitor/index.jsx b/Client/src/Pages/Monitors/CreateMonitor/index.jsx index ac68ea7eb..ac6e1258c 100644 --- a/Client/src/Pages/Monitors/CreateMonitor/index.jsx +++ b/Client/src/Pages/Monitors/CreateMonitor/index.jsx @@ -124,46 +124,58 @@ 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 { error } = monitorValidation.validate(form, { - abortEarly: false, - }); + const formattedUrl = monitor.type === "http" + ? `http${https ? "s" : ""}://` + monitor.url + : monitor.url; - 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." }); + 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; + }); + 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." }); + } + } + } catch (error) { + createToast({ body: "The endpoint you entered doesn't resolve. Check the URL again." }); + setErrors({ url: "The entered URL is not reachable." }); } }; @@ -375,7 +387,7 @@ const CreateMonitor = () => { onClick={handleCreateMonitor} disabled={Object.keys(errors).length !== 0 && true} > - Create monitor + {monitor.type === "http" ? "Create Monitor" : "Check Endpoint"} From ebf6a0ceb718527a8bcb412d20a189d9e2ac745a Mon Sep 17 00:00:00 2001 From: om-3004 Date: Tue, 15 Oct 2024 17:20:54 +0530 Subject: [PATCH 02/21] 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"]), From d1e65f53ba723c5b7ac58f89c2a39b9b3dc8dd14 Mon Sep 17 00:00:00 2001 From: om-3004 Date: Wed, 16 Oct 2024 18:28:04 +0530 Subject: [PATCH 03/21] Add test cases for checking monitor endpoint resolution --- Server/controllers/monitorController.js | 9 ++-- .../controllers/monitorController.test.js | 54 +++++++++++++++++++ 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index e6a924863..0586d650e 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -274,7 +274,6 @@ const createMonitor = async (req, res, next) => { * @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; @@ -284,12 +283,12 @@ const checkEndpointResolution = async (req, res, next) => { // 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(400).json({ + success: false, + msg: `DNS resolution failed`, + }); } return res.status(200).json({ success: true, diff --git a/Server/tests/controllers/monitorController.test.js b/Server/tests/controllers/monitorController.test.js index e2855e81b..730deb327 100644 --- a/Server/tests/controllers/monitorController.test.js +++ b/Server/tests/controllers/monitorController.test.js @@ -6,6 +6,7 @@ const { getMonitorsAndSummaryByTeamId, getMonitorsByTeamId, createMonitor, + checkEndpointResolution, deleteMonitor, deleteAllMonitors, editMonitor, @@ -21,6 +22,7 @@ const monitorController = proxyquire("../../controllers/monitorController", { "ssl-checker": sslCheckerStub, }); const logger = require("../../utils/logger"); +const dns = require("dns"); const SERVICE_NAME = "monitorController"; describe("Monitor Controller - getAllMonitors", () => { @@ -473,6 +475,58 @@ describe("Monitor Controller - createMonitor", () => { }); }); +describe('checkEndpointResolution', () => { + let dnsResolveStub; + beforeEach(() => { + req = { query: { monitorURL: 'example.com' } }; + res = { status: sinon.stub().returnsThis(), json: sinon.stub() }; + next = sinon.stub(); + dnsResolveStub = sinon.stub(dns, 'resolve'); + }); + afterEach(() => { + dnsResolveStub.restore(); + }); + it('should resolve the URL successfully', async () => { + dnsResolveStub.callsFake((monitorURL, callback) => callback(null)); + await checkEndpointResolution(req, res, next); + expect(res.status.calledWith(200)).to.be.true; + expect(res.json.calledWith({ + success: true, + msg: 'URL resolved successfully', + })).to.be.true; + expect(next.called).to.be.false; + }); + it("should return a 400 error message if DNS resolution fails", async () => { + const dnsError = new Error("DNS resolution failed"); + dnsResolveStub.callsFake((monitorURL, callback) => callback(dnsError)); + await checkEndpointResolution(req, res, next); + expect(res.status.calledOnceWith(400)).to.be.true; + expect(res.json.calledOnceWith({ + success: false, + msg: "DNS resolution failed", + })).to.be.true; + expect(next.notCalled).to.be.true; + }); + it("should remove the trailing slash and resolve the URL successfully", async () => { + req.query.monitorURL = 'http://example.com/'; // URL with a trailing slash + dnsResolveStub.callsFake((monitorURL, callback) => callback(null)); + await checkEndpointResolution(req, res, next); + expect(res.status.calledWith(200)).to.be.true; + expect(res.json.calledWith({ + success: true, + msg: 'URL resolved successfully', + })).to.be.true; + expect(next.called).to.be.false; + }); + it("should call next with an error if an exception is thrown", async () => { + req.query = {}; // Missing monitorURL will cause an error + await checkEndpointResolution(req, res, next); + expect(next.calledOnce).to.be.true; + const error = next.getCall(0).args[0]; + expect(error).to.be.an.instanceOf(Error); + }); +}); + describe("Monitor Controller - deleteMonitor", () => { beforeEach(() => { req = { From 6715a8b500a079a0a5c13e29593b197180977d8c Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 10:02:01 +0530 Subject: [PATCH 04/21] Refactor code for check endpoint and update test cases accordingly --- Server/controllers/monitorController.js | 9 ++---- .../controllers/monitorController.test.js | 30 +++++++++---------- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index 0586d650e..80cc1eccf 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -277,13 +277,8 @@ const createMonitor = async (req, res, next) => { 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.resolve(monitorURL, (error) => { + monitorURL = new URL(monitorURL); + dns.resolve(monitorURL.hostname, (error) => { if (error) { return res.status(400).json({ success: false, diff --git a/Server/tests/controllers/monitorController.test.js b/Server/tests/controllers/monitorController.test.js index 730deb327..b446edf73 100644 --- a/Server/tests/controllers/monitorController.test.js +++ b/Server/tests/controllers/monitorController.test.js @@ -478,7 +478,7 @@ describe("Monitor Controller - createMonitor", () => { describe('checkEndpointResolution', () => { let dnsResolveStub; beforeEach(() => { - req = { query: { monitorURL: 'example.com' } }; + req = { query: { monitorURL: 'https://example.com' } }; res = { status: sinon.stub().returnsThis(), json: sinon.stub() }; next = sinon.stub(); dnsResolveStub = sinon.stub(dns, 'resolve'); @@ -487,7 +487,7 @@ describe('checkEndpointResolution', () => { dnsResolveStub.restore(); }); it('should resolve the URL successfully', async () => { - dnsResolveStub.callsFake((monitorURL, callback) => callback(null)); + dnsResolveStub.callsFake((hostname, callback) => callback(null)); await checkEndpointResolution(req, res, next); expect(res.status.calledWith(200)).to.be.true; expect(res.json.calledWith({ @@ -498,7 +498,7 @@ describe('checkEndpointResolution', () => { }); it("should return a 400 error message if DNS resolution fails", async () => { const dnsError = new Error("DNS resolution failed"); - dnsResolveStub.callsFake((monitorURL, callback) => callback(dnsError)); + dnsResolveStub.callsFake((hostname, callback) => callback(dnsError)); await checkEndpointResolution(req, res, next); expect(res.status.calledOnceWith(400)).to.be.true; expect(res.json.calledOnceWith({ @@ -507,23 +507,21 @@ describe('checkEndpointResolution', () => { })).to.be.true; expect(next.notCalled).to.be.true; }); - it("should remove the trailing slash and resolve the URL successfully", async () => { - req.query.monitorURL = 'http://example.com/'; // URL with a trailing slash - dnsResolveStub.callsFake((monitorURL, callback) => callback(null)); - await checkEndpointResolution(req, res, next); - expect(res.status.calledWith(200)).to.be.true; - expect(res.json.calledWith({ - success: true, - msg: 'URL resolved successfully', - })).to.be.true; - expect(next.called).to.be.false; - }); - it("should call next with an error if an exception is thrown", async () => { - req.query = {}; // Missing monitorURL will cause an error + it('should call next with an error for invalid monitorURL', async () => { + req.query.monitorURL = 'invalid-url'; await checkEndpointResolution(req, res, next); expect(next.calledOnce).to.be.true; const error = next.getCall(0).args[0]; expect(error).to.be.an.instanceOf(Error); + expect(error.message).to.include('Invalid URL'); + }); + it('should handle an edge case where monitorURL is not provided', async () => { + req.query = {}; + await checkEndpointResolution(req, res, next); + expect(next.calledOnce).to.be.true; + const error = next.getCall(0).args[0]; + expect(error).to.be.an.instanceOf(Error); + expect(error.message).to.include('Invalid URL'); }); }); From 3de2106c70a796032c456accb67ef1f2880aba80 Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 10:09:50 +0530 Subject: [PATCH 05/21] Add api documentation for check resolution endpoint create --- Server/openapi.json | 55 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/Server/openapi.json b/Server/openapi.json index 6d079e685..aeb7285a8 100644 --- a/Server/openapi.json +++ b/Server/openapi.json @@ -845,6 +845,61 @@ ] } }, + "/monitors//check-endpoint/url": { + "get": { + "tags": ["monitors"], + "description": "Check DNS resolution for a given URL", + "parameters": [ + { + "name": "monitorURL", + "in": "query", + "required": true, + "schema": { + "type": "string", + "example": "https://example.com" + }, + "description": "The URL to check DNS resolution for" + } + ], + "responses": { + "200": { + "description": "URL resolved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse" + } + } + } + }, + "400": { + "description": "DNS resolution failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, "/monitors/{monitorId}": { "get": { "tags": ["monitors"], From 9bc42cbf4fa51a0e6ac95f3f493330587f87028f Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 10:14:34 +0530 Subject: [PATCH 06/21] Add resolution check before adding PageSpeed monitors --- Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx index e855328da..802572782 100644 --- a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx +++ b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx @@ -5,6 +5,7 @@ import { monitorValidation } from "../../../Validation/validation"; import { useNavigate } from "react-router-dom"; import { useTheme } from "@emotion/react"; import { createPageSpeed } from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice"; +import { checkEndpointResolution } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice" import { createToast } from "../../../Utils/toastUtils"; import { logger } from "../../../Utils/Logger"; import { ConfigBox } from "../../Monitors/styled"; @@ -111,6 +112,15 @@ const CreatePageSpeed = () => { 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, From d61a41fd95392f30f28f692ab9f440271d6df08d Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 10:18:12 +0530 Subject: [PATCH 07/21] Update the api endpoint from /check-endpoint/url to /resolution/url --- Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js | 2 +- Client/src/Pages/Monitors/CreateMonitor/index.jsx | 2 +- Client/src/Utils/NetworkService.js | 2 +- Server/openapi.json | 2 +- Server/routes/monitorRoute.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js index 05ec9b316..81e4d4b1b 100644 --- a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js +++ b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js @@ -32,7 +32,7 @@ export const createUptimeMonitor = createAsyncThunk( ); export const checkEndpointResolution = createAsyncThunk( - "monitors/checkEnpoint", + "monitors/checkEndpoint", async(data, thunkApi) => { try{ const { authToken, monitorURL } = data; diff --git a/Client/src/Pages/Monitors/CreateMonitor/index.jsx b/Client/src/Pages/Monitors/CreateMonitor/index.jsx index 50d4e3195..26a233800 100644 --- a/Client/src/Pages/Monitors/CreateMonitor/index.jsx +++ b/Client/src/Pages/Monitors/CreateMonitor/index.jsx @@ -385,7 +385,7 @@ const CreateMonitor = () => { onClick={handleCreateMonitor} disabled={Object.keys(errors).length !== 0 && true} > - Create Monitor + Create monitor diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js index a9dd80731..22717b364 100644 --- a/Client/src/Utils/NetworkService.js +++ b/Client/src/Utils/NetworkService.js @@ -107,7 +107,7 @@ class NetworkService { if (monitorURL) params.append("monitorURL", monitorURL); - return this.axiosInstance.get(`/monitors/check-endpoint/url?${params.toString()}`, { + return this.axiosInstance.get(`/monitors/resolution/url?${params.toString()}`, { headers: { Authorization: `Bearer ${authToken}`, "Content-Type": "application/json", diff --git a/Server/openapi.json b/Server/openapi.json index aeb7285a8..ff13498ea 100644 --- a/Server/openapi.json +++ b/Server/openapi.json @@ -845,7 +845,7 @@ ] } }, - "/monitors//check-endpoint/url": { + "/monitors/resolution/url": { "get": { "tags": ["monitors"], "description": "Check DNS resolution for a given URL", diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js index c3f695556..67ab8a5e6 100644 --- a/Server/routes/monitorRoute.js +++ b/Server/routes/monitorRoute.js @@ -19,7 +19,7 @@ router.post( ); router.get( - "/check-endpoint/url", + "/resolution/url", isAllowed(["admin", "superadmin"]), monitorController.checkEndpointResolution ) From 0ad94ecefce4156a593fcd1ff6d559522cfe8f2f Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 10:25:08 +0530 Subject: [PATCH 08/21] Bypass the resolution check when Ping Monitoring is selected --- .../src/Pages/Monitors/CreateMonitor/index.jsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Client/src/Pages/Monitors/CreateMonitor/index.jsx b/Client/src/Pages/Monitors/CreateMonitor/index.jsx index 26a233800..d9cdb725f 100644 --- a/Client/src/Pages/Monitors/CreateMonitor/index.jsx +++ b/Client/src/Pages/Monitors/CreateMonitor/index.jsx @@ -149,13 +149,15 @@ const CreateMonitor = () => { 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; + if (monitor.type === "http") { + 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 = { From afe7c9047c1ae904ac30a4ae35b4d69cb647eb5b Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 10:47:34 +0530 Subject: [PATCH 09/21] Handle imports exports format check resolution tests code --- Server/controllers/monitorController.js | 1 + Server/routes/monitorRoute.js | 5 +- .../controllers/monitorController.test.js | 95 ++++++++++--------- 3 files changed, 52 insertions(+), 49 deletions(-) diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index 905a4b9bd..93d4e4f1c 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -514,6 +514,7 @@ export { getMonitorsAndSummaryByTeamId, getMonitorsByTeamId, createMonitor, + checkEndpointResolution, deleteMonitor, deleteAllMonitors, editMonitor, diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js index f45f3ba33..a0ab11d84 100644 --- a/Server/routes/monitorRoute.js +++ b/Server/routes/monitorRoute.js @@ -7,6 +7,7 @@ import { getMonitorsAndSummaryByTeamId, getMonitorsByTeamId, createMonitor, + checkEndpointResolution, deleteMonitor, deleteAllMonitors, editMonitor, @@ -30,13 +31,13 @@ router.get("/team/:teamId", getMonitorsByTeamId); router.get( "/resolution/url", isAllowed(["admin", "superadmin"]), - monitorController.checkEndpointResolution + checkEndpointResolution ) router.delete( "/:monitorId", isAllowed(["admin", "superadmin"]), - monitorController.deleteMonitor + deleteMonitor ); router.post("/", isAllowed(["admin", "superadmin"]), createMonitor); diff --git a/Server/tests/controllers/monitorController.test.js b/Server/tests/controllers/monitorController.test.js index eb8c8e44a..d05685cdb 100644 --- a/Server/tests/controllers/monitorController.test.js +++ b/Server/tests/controllers/monitorController.test.js @@ -6,6 +6,7 @@ import { getMonitorsAndSummaryByTeamId, getMonitorsByTeamId, createMonitor, + checkEndpointResolution, deleteMonitor, deleteAllMonitors, editMonitor, @@ -461,53 +462,53 @@ describe("Monitor Controller - createMonitor", () => { }); describe('checkEndpointResolution', () => { - let dnsResolveStub; - beforeEach(() => { - req = { query: { monitorURL: 'https://example.com' } }; - res = { status: sinon.stub().returnsThis(), json: sinon.stub() }; - next = sinon.stub(); - dnsResolveStub = sinon.stub(dns, 'resolve'); - }); - afterEach(() => { - dnsResolveStub.restore(); - }); - it('should resolve the URL successfully', async () => { - dnsResolveStub.callsFake((hostname, callback) => callback(null)); - await checkEndpointResolution(req, res, next); - expect(res.status.calledWith(200)).to.be.true; - expect(res.json.calledWith({ - success: true, - msg: 'URL resolved successfully', - })).to.be.true; - expect(next.called).to.be.false; - }); - it("should return a 400 error message if DNS resolution fails", async () => { - const dnsError = new Error("DNS resolution failed"); - dnsResolveStub.callsFake((hostname, callback) => callback(dnsError)); - await checkEndpointResolution(req, res, next); - expect(res.status.calledOnceWith(400)).to.be.true; - expect(res.json.calledOnceWith({ - success: false, - msg: "DNS resolution failed", - })).to.be.true; - expect(next.notCalled).to.be.true; - }); - it('should call next with an error for invalid monitorURL', async () => { - req.query.monitorURL = 'invalid-url'; - await checkEndpointResolution(req, res, next); - expect(next.calledOnce).to.be.true; - const error = next.getCall(0).args[0]; - expect(error).to.be.an.instanceOf(Error); - expect(error.message).to.include('Invalid URL'); - }); - it('should handle an edge case where monitorURL is not provided', async () => { - req.query = {}; - await checkEndpointResolution(req, res, next); - expect(next.calledOnce).to.be.true; - const error = next.getCall(0).args[0]; - expect(error).to.be.an.instanceOf(Error); - expect(error.message).to.include('Invalid URL'); - }); + let req, res, next, dnsResolveStub; + beforeEach(() => { + req = { query: { monitorURL: 'https://example.com' } }; + res = { status: sinon.stub().returnsThis(), json: sinon.stub() }; + next = sinon.stub(); + dnsResolveStub = sinon.stub(dns, 'resolve'); + }); + afterEach(() => { + dnsResolveStub.restore(); + }); + it('should resolve the URL successfully', async () => { + dnsResolveStub.callsFake((hostname, callback) => callback(null)); + await checkEndpointResolution(req, res, next); + expect(res.status.calledWith(200)).to.be.true; + expect(res.json.calledWith({ + success: true, + msg: 'URL resolved successfully', + })).to.be.true; + expect(next.called).to.be.false; + }); + it("should return a 400 error message if DNS resolution fails", async () => { + const dnsError = new Error("DNS resolution failed"); + dnsResolveStub.callsFake((hostname, callback) => callback(dnsError)); + await checkEndpointResolution(req, res, next); + expect(res.status.calledOnceWith(400)).to.be.true; + expect(res.json.calledOnceWith({ + success: false, + msg: "DNS resolution failed", + })).to.be.true; + expect(next.notCalled).to.be.true; + }); + it('should call next with an error for invalid monitorURL', async () => { + req.query.monitorURL = 'invalid-url'; + await checkEndpointResolution(req, res, next); + expect(next.calledOnce).to.be.true; + const error = next.getCall(0).args[0]; + expect(error).to.be.an.instanceOf(Error); + expect(error.message).to.include('Invalid URL'); + }); + it('should handle an edge case where monitorURL is not provided', async () => { + req.query = {}; + await checkEndpointResolution(req, res, next); + expect(next.calledOnce).to.be.true; + const error = next.getCall(0).args[0]; + expect(error).to.be.an.instanceOf(Error); + expect(error.message).to.include('Invalid URL'); + }); }); describe("Monitor Controller - deleteMonitor", () => { From 7ec5e64f6fe84eef97af8db29e845eafbd957046 Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 10:50:19 +0530 Subject: [PATCH 10/21] Remove unused imports --- Client/src/Utils/NetworkService.js | 1 - Server/controllers/monitorController.js | 1 - Server/tests/controllers/monitorController.test.js | 2 -- 3 files changed, 4 deletions(-) diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js index a91a6cbe6..ff509a0e9 100644 --- a/Client/src/Utils/NetworkService.js +++ b/Client/src/Utils/NetworkService.js @@ -3,7 +3,6 @@ const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL; const FALLBACK_BASE_URL = "http://localhost:5000/api/v1"; import { clearAuthState } from "../Features/Auth/authSlice"; import { clearUptimeMonitorState } from "../Features/UptimeMonitors/uptimeMonitorsSlice"; -import { logger } from "./Logger"; class NetworkService { constructor(store, dispatch, navigate) { this.store = store; diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index 93d4e4f1c..d72a8254b 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -21,7 +21,6 @@ import logger from "../utils/logger.js"; import { handleError, handleValidationError, - fetchMonitorCertificate, } from "./controllerUtils.js"; import dns from "dns"; diff --git a/Server/tests/controllers/monitorController.test.js b/Server/tests/controllers/monitorController.test.js index d05685cdb..503a2c5cb 100644 --- a/Server/tests/controllers/monitorController.test.js +++ b/Server/tests/controllers/monitorController.test.js @@ -17,8 +17,6 @@ import jwt from "jsonwebtoken"; import sinon from "sinon"; import { successMessages } from "../../utils/messages.js"; import logger from "../../utils/logger.js"; -// import * as monitorController from "../../controllers/monitorController.js"; -// import { fetchMonitorCertificate } from "../../controllers/controllerUtils.js"; import dns from "dns"; const SERVICE_NAME = "monitorController"; From 0f763e7913778e3ca6b9e57b81b44a7dce4b4639 Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 11:06:46 +0530 Subject: [PATCH 11/21] promisify the resolve method to pass the error to the middleware and updat test cases accordingly --- Server/controllers/monitorController.js | 20 +++++++++---------- .../controllers/monitorController.test.js | 10 ++++------ 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index d72a8254b..5ee6d2c5d 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -276,18 +276,18 @@ const checkEndpointResolution = async (req, res, next) => { try { let { monitorURL } = req.query; monitorURL = new URL(monitorURL); - dns.resolve(monitorURL.hostname, (error) => { - if (error) { - return res.status(400).json({ - success: false, - msg: `DNS resolution failed`, - }); - } - return res.status(200).json({ - success: true, - msg: `URL resolved successfully`, + await new Promise((resolve, reject) => { + dns.resolve(monitorURL.hostname, (error) => { + if (error) { + reject(error); + } + resolve(); }); }); + return res.status(200).json({ + success: true, + msg: `URL resolved successfully`, + }); } catch (error) { next(handleError(error, SERVICE_NAME, "checkEndpointResolution")); } diff --git a/Server/tests/controllers/monitorController.test.js b/Server/tests/controllers/monitorController.test.js index 503a2c5cb..f83d74396 100644 --- a/Server/tests/controllers/monitorController.test.js +++ b/Server/tests/controllers/monitorController.test.js @@ -484,12 +484,10 @@ describe('checkEndpointResolution', () => { const dnsError = new Error("DNS resolution failed"); dnsResolveStub.callsFake((hostname, callback) => callback(dnsError)); await checkEndpointResolution(req, res, next); - expect(res.status.calledOnceWith(400)).to.be.true; - expect(res.json.calledOnceWith({ - success: false, - msg: "DNS resolution failed", - })).to.be.true; - expect(next.notCalled).to.be.true; + expect(next.calledOnce).to.be.true; + const errorPassedToNext = next.getCall(0).args[0]; + expect(errorPassedToNext).to.be.an.instanceOf(Error); + expect(errorPassedToNext.message).to.include('DNS resolution failed'); }); it('should call next with an error for invalid monitorURL', async () => { req.query.monitorURL = 'invalid-url'; From 5d3e1796e0bdd4dce73d31cead821c760c0f5406 Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 11:09:23 +0530 Subject: [PATCH 12/21] Update the test name to follow the naming convention --- Server/tests/controllers/monitorController.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/tests/controllers/monitorController.test.js b/Server/tests/controllers/monitorController.test.js index f83d74396..26c6a6048 100644 --- a/Server/tests/controllers/monitorController.test.js +++ b/Server/tests/controllers/monitorController.test.js @@ -459,7 +459,7 @@ describe("Monitor Controller - createMonitor", () => { }); }); -describe('checkEndpointResolution', () => { +describe("Monitor Controllor - checkEndpointResolution", () => { let req, res, next, dnsResolveStub; beforeEach(() => { req = { query: { monitorURL: 'https://example.com' } }; From 7f0d5d42551a8c63ec01f21d79a32875083eb21c Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 11:16:49 +0530 Subject: [PATCH 13/21] Update test case to check for the specific errors and remove test case for empty URL --- Server/tests/controllers/monitorController.test.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/Server/tests/controllers/monitorController.test.js b/Server/tests/controllers/monitorController.test.js index 26c6a6048..690649660 100644 --- a/Server/tests/controllers/monitorController.test.js +++ b/Server/tests/controllers/monitorController.test.js @@ -482,27 +482,21 @@ describe("Monitor Controllor - checkEndpointResolution", () => { }); it("should return a 400 error message if DNS resolution fails", async () => { const dnsError = new Error("DNS resolution failed"); + dnsError.code = 'ENOTFOUND'; dnsResolveStub.callsFake((hostname, callback) => callback(dnsError)); await checkEndpointResolution(req, res, next); expect(next.calledOnce).to.be.true; const errorPassedToNext = next.getCall(0).args[0]; expect(errorPassedToNext).to.be.an.instanceOf(Error); - expect(errorPassedToNext.message).to.include('DNS resolution failed'); + expect(errorPassedToNext.message).to.include('DNS resolution failed'); + expect(errorPassedToNext.code).to.equal('ENOTFOUND'); }); it('should call next with an error for invalid monitorURL', async () => { req.query.monitorURL = 'invalid-url'; await checkEndpointResolution(req, res, next); expect(next.calledOnce).to.be.true; const error = next.getCall(0).args[0]; - expect(error).to.be.an.instanceOf(Error); - expect(error.message).to.include('Invalid URL'); - }); - it('should handle an edge case where monitorURL is not provided', async () => { - req.query = {}; - await checkEndpointResolution(req, res, next); - expect(next.calledOnce).to.be.true; - const error = next.getCall(0).args[0]; - expect(error).to.be.an.instanceOf(Error); + expect(error).to.be.an.instanceOf(TypeError); expect(error.message).to.include('Invalid URL'); }); }); From aee7e02587aa16d2134fa6a62e7f9a6fff4a58c3 Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 12:35:15 +0530 Subject: [PATCH 14/21] Add validation for monitor URL and update test case and api documentation accordingly --- Server/controllers/monitorController.js | 8 ++++++++ Server/openapi.json | 10 ++++++++++ Server/tests/controllers/monitorController.test.js | 8 ++++---- Server/validation/joi.js | 7 ++++++- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index 5ee6d2c5d..c56e8851b 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -3,6 +3,7 @@ import { getMonitorByIdQueryValidation, getMonitorsByTeamIdValidation, createMonitorBodyValidation, + getMonitorURLByQueryValidation, editMonitorBodyValidation, getMonitorsAndSummaryByTeamIdParamValidation, getMonitorsAndSummaryByTeamIdQueryValidation, @@ -273,6 +274,13 @@ const createMonitor = async (req, res, next) => { * @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 { let { monitorURL } = req.query; monitorURL = new URL(monitorURL); diff --git a/Server/openapi.json b/Server/openapi.json index ff13498ea..aaa10a5d3 100644 --- a/Server/openapi.json +++ b/Server/openapi.json @@ -882,6 +882,16 @@ } } }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal Server Error", "content": { diff --git a/Server/tests/controllers/monitorController.test.js b/Server/tests/controllers/monitorController.test.js index 690649660..bd89b3dea 100644 --- a/Server/tests/controllers/monitorController.test.js +++ b/Server/tests/controllers/monitorController.test.js @@ -480,7 +480,7 @@ describe("Monitor Controllor - checkEndpointResolution", () => { })).to.be.true; expect(next.called).to.be.false; }); - it("should return a 400 error message if DNS resolution fails", async () => { + it("should return an error if DNS resolution fails", async () => { const dnsError = new Error("DNS resolution failed"); dnsError.code = 'ENOTFOUND'; dnsResolveStub.callsFake((hostname, callback) => callback(dnsError)); @@ -491,13 +491,13 @@ describe("Monitor Controllor - checkEndpointResolution", () => { expect(errorPassedToNext.message).to.include('DNS resolution failed'); expect(errorPassedToNext.code).to.equal('ENOTFOUND'); }); - it('should call next with an error for invalid monitorURL', async () => { + it('should reject with an error if query validation fails', async () => { req.query.monitorURL = 'invalid-url'; await checkEndpointResolution(req, res, next); expect(next.calledOnce).to.be.true; const error = next.getCall(0).args[0]; - expect(error).to.be.an.instanceOf(TypeError); - expect(error.message).to.include('Invalid URL'); + expect(next.firstCall.args[0]).to.be.an("error"); + expect(next.firstCall.args[0].status).to.equal(422); }); }); diff --git a/Server/validation/joi.js b/Server/validation/joi.js index 033650a3d..090cb3211 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -236,12 +236,16 @@ const pauseMonitorParamValidation = joi.object({ monitorId: joi.string().required(), }); +const getMonitorURLByQueryValidation = joi.object({ + monitorURL: joi.string().uri().required(), +}); + //**************************************** // Alerts //**************************************** const createAlertParamValidation = joi.object({ - monitorId: joi.string().required(), + monitorId: joi.string().uri().required(), }); const createAlertBodyValidation = joi.object({ @@ -446,6 +450,7 @@ export { getCertificateParamValidation, editMonitorBodyValidation, pauseMonitorParamValidation, + getMonitorURLByQueryValidation, editUserParamValidation, editUserBodyValidation, createAlertParamValidation, From 8819b0ab78fc4758e2f8b3b2f197876823af678d Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 13:00:11 +0530 Subject: [PATCH 15/21] Update Button to show Checking endpoint during the endpoint checking process in Create Monitor as well as PageSpeed --- Client/src/Pages/Monitors/CreateMonitor/index.jsx | 6 +++++- Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Client/src/Pages/Monitors/CreateMonitor/index.jsx b/Client/src/Pages/Monitors/CreateMonitor/index.jsx index d9cdb725f..e919b1b28 100644 --- a/Client/src/Pages/Monitors/CreateMonitor/index.jsx +++ b/Client/src/Pages/Monitors/CreateMonitor/index.jsx @@ -43,6 +43,7 @@ const CreateMonitor = () => { }); const [https, setHttps] = useState(true); const [errors, setErrors] = useState({}); + const [isCheckingEndpoint, setIsCheckingEndpoint] = useState(false); useEffect(() => { const fetchMonitor = async () => { @@ -125,6 +126,7 @@ const CreateMonitor = () => { const handleCreateMonitor = async (event) => { event.preventDefault(); + setIsCheckingEndpoint(true); //obj to submit let form = { url: @@ -156,8 +158,10 @@ const CreateMonitor = () => { 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." }); + setIsCheckingEndpoint(false); return; } + setIsCheckingEndpoint(false); } form = { @@ -387,7 +391,7 @@ const CreateMonitor = () => { onClick={handleCreateMonitor} disabled={Object.keys(errors).length !== 0 && true} > - Create monitor + { isCheckingEndpoint ? "Checking endpoint" : "Create monitor" } diff --git a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx index 802572782..6bdbaacc9 100644 --- a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx +++ b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx @@ -40,6 +40,7 @@ const CreatePageSpeed = () => { }); const [https, setHttps] = useState(true); const [errors, setErrors] = useState({}); + const [isCheckingEndpoint, setIsCheckingEndpoint] = useState(false); const handleChange = (event, name) => { const { value, id } = event.target; @@ -92,6 +93,7 @@ const CreatePageSpeed = () => { const handleCreateMonitor = async (event) => { event.preventDefault(); + setIsCheckingEndpoint(true); //obj to submit let form = { url: `http${https ? "s" : ""}://` + monitor.url, @@ -118,8 +120,10 @@ const CreatePageSpeed = () => { 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." }); + setIsCheckingEndpoint(false); return; } + setIsCheckingEndpoint(false); form = { ...form, @@ -343,7 +347,7 @@ const CreatePageSpeed = () => { onClick={handleCreateMonitor} disabled={Object.keys(errors).length !== 0 && true} > - Create monitor + { isCheckingEndpoint ? "Checking endpoint" : "Create monitor" } From cb64d16d692e734ad5364e67829b4d82d7893b36 Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 14:21:47 +0530 Subject: [PATCH 16/21] Remove the extra uri in validation which was added by mistake --- Server/validation/joi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/validation/joi.js b/Server/validation/joi.js index 090cb3211..1aaab0041 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -245,7 +245,7 @@ const getMonitorURLByQueryValidation = joi.object({ //**************************************** const createAlertParamValidation = joi.object({ - monitorId: joi.string().uri().required(), + monitorId: joi.string().required(), }); const createAlertBodyValidation = joi.object({ From ff6051b6b0a7e28237d04bc892447857e9c16029 Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 16:59:49 +0530 Subject: [PATCH 17/21] Update test case for url resolution to make it more specific --- Server/controllers/monitorController.js | 2 +- Server/tests/controllers/monitorController.test.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index c56e8851b..302e0ff11 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -264,7 +264,7 @@ const createMonitor = async (req, res, next) => { }; /** - * Checks if the endpoint can be resolved and adds it to the job queue. + * 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. diff --git a/Server/tests/controllers/monitorController.test.js b/Server/tests/controllers/monitorController.test.js index bd89b3dea..0a9dbafe6 100644 --- a/Server/tests/controllers/monitorController.test.js +++ b/Server/tests/controllers/monitorController.test.js @@ -490,6 +490,7 @@ describe("Monitor Controllor - checkEndpointResolution", () => { expect(errorPassedToNext).to.be.an.instanceOf(Error); expect(errorPassedToNext.message).to.include('DNS resolution failed'); expect(errorPassedToNext.code).to.equal('ENOTFOUND'); + expect(errorPassedToNext.status).to.equal(500); }); it('should reject with an error if query validation fails', async () => { req.query.monitorURL = 'invalid-url'; @@ -498,6 +499,7 @@ describe("Monitor Controllor - checkEndpointResolution", () => { const error = next.getCall(0).args[0]; expect(next.firstCall.args[0]).to.be.an("error"); expect(next.firstCall.args[0].status).to.equal(422); + expect(error.message).to.equal('"monitorURL" must be a valid uri'); }); }); From 9de2dfe514082a1c932500ee57e23ab630fa585b Mon Sep 17 00:00:00 2001 From: om-3004 Date: Thu, 17 Oct 2024 17:27:40 +0530 Subject: [PATCH 18/21] Remove duplicate api endpoint for deleting monitor --- Server/routes/monitorRoute.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js index a0ab11d84..f306a5146 100644 --- a/Server/routes/monitorRoute.js +++ b/Server/routes/monitorRoute.js @@ -42,8 +42,6 @@ router.delete( router.post("/", isAllowed(["admin", "superadmin"]), createMonitor); -router.delete("/:monitorId", isAllowed(["admin", "superadmin"]), deleteMonitor); - router.put("/:monitorId", isAllowed(["admin", "superadmin"]), editMonitor); router.delete("/", isAllowed(["superadmin"]), deleteAllMonitors); From 6c40500153c6686c28b37860b97e00329dc4f598 Mon Sep 17 00:00:00 2001 From: om-3004 Date: Fri, 18 Oct 2024 11:09:52 +0530 Subject: [PATCH 19/21] Add loading button while the url is getting resolved --- Client/src/Pages/Monitors/CreateMonitor/index.jsx | 14 ++++++-------- .../src/Pages/PageSpeed/CreatePageSpeed/index.jsx | 13 ++++++------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/Client/src/Pages/Monitors/CreateMonitor/index.jsx b/Client/src/Pages/Monitors/CreateMonitor/index.jsx index e919b1b28..49e4b09a5 100644 --- a/Client/src/Pages/Monitors/CreateMonitor/index.jsx +++ b/Client/src/Pages/Monitors/CreateMonitor/index.jsx @@ -1,5 +1,6 @@ import { useState, useEffect } from "react"; import { Box, Button, ButtonGroup, Stack, Typography } from "@mui/material"; +import LoadingButton from '@mui/lab/LoadingButton'; import { useSelector, useDispatch } from "react-redux"; import { monitorValidation } from "../../../Validation/validation"; import { createUptimeMonitor } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; @@ -20,7 +21,7 @@ import "./index.css"; const CreateMonitor = () => { const MS_PER_MINUTE = 60000; const { user, authToken } = useSelector((state) => state.auth); - const { monitors } = useSelector((state) => state.uptimeMonitors); + const { monitors, isLoading } = useSelector((state) => state.uptimeMonitors); const dispatch = useDispatch(); const navigate = useNavigate(); const theme = useTheme(); @@ -43,7 +44,6 @@ const CreateMonitor = () => { }); const [https, setHttps] = useState(true); const [errors, setErrors] = useState({}); - const [isCheckingEndpoint, setIsCheckingEndpoint] = useState(false); useEffect(() => { const fetchMonitor = async () => { @@ -126,7 +126,6 @@ const CreateMonitor = () => { const handleCreateMonitor = async (event) => { event.preventDefault(); - setIsCheckingEndpoint(true); //obj to submit let form = { url: @@ -158,10 +157,8 @@ const CreateMonitor = () => { 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." }); - setIsCheckingEndpoint(false); return; } - setIsCheckingEndpoint(false); } form = { @@ -385,14 +382,15 @@ const CreateMonitor = () => { - + Create monitor + diff --git a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx index 6bdbaacc9..798818d1f 100644 --- a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx +++ b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx @@ -1,5 +1,6 @@ import { useState } from "react"; import { Box, Button, ButtonGroup, Stack, Typography } from "@mui/material"; +import LoadingButton from '@mui/lab/LoadingButton'; import { useSelector, useDispatch } from "react-redux"; import { monitorValidation } from "../../../Validation/validation"; import { useNavigate } from "react-router-dom"; @@ -19,6 +20,7 @@ import "./index.css"; const CreatePageSpeed = () => { const MS_PER_MINUTE = 60000; const { user, authToken } = useSelector((state) => state.auth); + const { isLoading } = useSelector((state) => state.uptimeMonitors); const dispatch = useDispatch(); const navigate = useNavigate(); const theme = useTheme(); @@ -40,7 +42,6 @@ const CreatePageSpeed = () => { }); const [https, setHttps] = useState(true); const [errors, setErrors] = useState({}); - const [isCheckingEndpoint, setIsCheckingEndpoint] = useState(false); const handleChange = (event, name) => { const { value, id } = event.target; @@ -93,7 +94,6 @@ const CreatePageSpeed = () => { const handleCreateMonitor = async (event) => { event.preventDefault(); - setIsCheckingEndpoint(true); //obj to submit let form = { url: `http${https ? "s" : ""}://` + monitor.url, @@ -120,10 +120,8 @@ const CreatePageSpeed = () => { 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." }); - setIsCheckingEndpoint(false); return; } - setIsCheckingEndpoint(false); form = { ...form, @@ -341,14 +339,15 @@ const CreatePageSpeed = () => { - + Create monitor + From eccf4a8111fb49ec4c23f9d272eb10d437556604 Mon Sep 17 00:00:00 2001 From: om-3004 Date: Sat, 19 Oct 2024 09:57:18 +0530 Subject: [PATCH 20/21] Change the loading state of page speed monitors from create monitor redux store to page speed monitor redux store --- Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx index 390fde36c..e0eed4472 100644 --- a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx +++ b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx @@ -20,7 +20,7 @@ import "./index.css"; const CreatePageSpeed = () => { const MS_PER_MINUTE = 60000; const { user, authToken } = useSelector((state) => state.auth); - const { isLoading } = useSelector((state) => state.uptimeMonitors); + const { isLoading } = useSelector((state) => state.pageSpeedMonitors); const dispatch = useDispatch(); const navigate = useNavigate(); const theme = useTheme(); From 9c0a2b5f35968ee79bac4b24f8e8af99c0cc7a45 Mon Sep 17 00:00:00 2001 From: om-3004 Date: Sat, 19 Oct 2024 11:05:36 +0530 Subject: [PATCH 21/21] Seperate check resolution for Page Speed Monitors --- .../PageSpeedMonitor/pageSpeedMonitorSlice.js | 43 ++++++++++++++++++- .../Pages/PageSpeed/CreatePageSpeed/index.jsx | 3 +- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js index 67d2c14f2..6cd91ac28 100644 --- a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js +++ b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js @@ -31,6 +31,30 @@ export const createPageSpeed = createAsyncThunk( } ); +export const checkEndpointResolution = createAsyncThunk( + "monitors/checkEndpoint", + 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 getPagespeedMonitorById = createAsyncThunk( "monitors/getMonitorById", async (data, thunkApi) => { @@ -222,7 +246,24 @@ const pageSpeedMonitorSlice = createSlice({ ? action.payload.msg : "Failed to create page speed 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"; + }) // ***************************************************** // Update Monitor // ***************************************************** diff --git a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx index e0eed4472..162b231a2 100644 --- a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx +++ b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx @@ -5,8 +5,7 @@ import { useSelector, useDispatch } from "react-redux"; import { monitorValidation } from "../../../Validation/validation"; import { useNavigate } from "react-router-dom"; import { useTheme } from "@emotion/react"; -import { createPageSpeed } from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice"; -import { checkEndpointResolution } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice" +import { createPageSpeed, checkEndpointResolution } from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice"; import { createToast } from "../../../Utils/toastUtils"; import { logger } from "../../../Utils/Logger"; import { ConfigBox } from "../../Monitors/styled";