diff --git a/Client/src/Components/Alert/index.jsx b/Client/src/Components/Alert/index.jsx index eb5fbd13b..cafd58451 100644 --- a/Client/src/Components/Alert/index.jsx +++ b/Client/src/Components/Alert/index.jsx @@ -31,7 +31,7 @@ const icons = { const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => { const theme = useTheme(); - const { text, light, border } = theme.palette[variant]; + const { text, bg, border } = theme.palette[variant]; const icon = icons[variant]; return ( @@ -45,7 +45,7 @@ const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => { padding: hasIcon ? theme.spacing(8) : `${theme.spacing(4)} ${theme.spacing(8)}`, - backgroundColor: light, + backgroundColor: bg, border: `solid 1px ${border}`, borderRadius: theme.shape.borderRadius, }} diff --git a/Client/src/Components/Charts/ResponseTimeChart/index.css b/Client/src/Components/Charts/ResponseTimeChart/index.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/Client/src/Components/Charts/ResponseTimeChart/index.jsx b/Client/src/Components/Charts/ResponseTimeChart/index.jsx deleted file mode 100644 index fd6c0f41e..000000000 --- a/Client/src/Components/Charts/ResponseTimeChart/index.jsx +++ /dev/null @@ -1,43 +0,0 @@ -import PropTypes from "prop-types"; -import { Stack } from "@mui/material"; -import { BarChart, Bar, ResponsiveContainer, Cell } from "recharts"; -import "./index.css"; - -const ResponseTimeChart = ({ checks = [] }) => { - return ( - - - - - {checks.map((check, index) => ( - - ))} - - - - - ); -}; - -ResponseTimeChart.propTypes = { - checks: PropTypes.array, -}; -export default ResponseTimeChart; diff --git a/Client/src/Components/Inputs/Autocomplete/index.jsx b/Client/src/Components/Inputs/Autocomplete/index.jsx new file mode 100644 index 000000000..5256efccc --- /dev/null +++ b/Client/src/Components/Inputs/Autocomplete/index.jsx @@ -0,0 +1,116 @@ +import { Autocomplete, TextField } from "@mui/material"; +import React from "react"; +import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; + +/** + * @example + * + * const options = [ + * { _id: "66d6119ef959cbc681e034f0", name: "Googler" }, + * { _id: "66d6119ef959cbc681e034f0", name: "CNN" }, + * { _id: "66d61a1bf959cbc681e0353f", name: "X Corp." }, + * ]; + * + * + * + */ + +/** + * AutoCompleteField component. + * + * @component + * @param {Object} props - The component props. + * @param {string} props.id - The ID of the autocomplete field. + * @param {string} props.type - The type of the input field (text or number). + * @param {Array} props.options - The options for the autocomplete field. + * @param {string} props.placeholder - The placeholder text for the input field. + * @param {boolean} props.disabled - Indicates if the field is disabled. + * @returns {JSX.Element} The AutoCompleteField component. + */ + +const AutoCompleteField = ({ + id, + type, + options, + placeholder = "Type to search", + disabled, +}) => { + const [value, setValue] = React.useState(); + const [inputValue, setInputValue] = React.useState(""); + const theme = useTheme(); + + return ( + { + setValue(newValue); + }} + inputValue={inputValue} + onInputChange={(event, newInputValue) => { + setInputValue(newInputValue); + }} + options={options} + getOptionLabel={(option) => option.name} + renderInput={(params) => ( + + )} + renderOption={(props, option) => { + const { key, ...optionProps } = props; + return ( +
  • +
    {{option.name}}
    +
  • + ); + }} + slotProps={{ + popper: { + sx: { + "& ul": { p: 0 }, + "& li": { borderRadius: theme.shape.borderRadius }, + }, + }, + paper: { + sx: { + p: 2, + fontSize: 13, + }, + }, + }} + /> + ); +}; + +AutoCompleteField.displayName = "AutoCompleteField"; + +AutoCompleteField.propTypes = { + id: PropTypes.string, + type: PropTypes.oneOf(["text", "number"]), + options: PropTypes.array, + placeholder: PropTypes.string, + disabled: PropTypes.bool, + setWidth: PropTypes.string, +}; + +export default AutoCompleteField; diff --git a/Client/src/Pages/Monitors/Home/host.jsx b/Client/src/Pages/Monitors/Home/host.jsx index d450a53f4..9e6a71a05 100644 --- a/Client/src/Pages/Monitors/Home/host.jsx +++ b/Client/src/Pages/Monitors/Home/host.jsx @@ -53,7 +53,8 @@ Host.propTypes = { params: PropTypes.shape({ title: PropTypes.string, percentageColor: PropTypes.string, - percentage: PropTypes.number, + percentage: PropTypes.string, + url: PropTypes.string, }).isRequired, }; diff --git a/Client/src/Pages/Monitors/Home/monitorData.jsx b/Client/src/Pages/Monitors/Home/monitorData.jsx index d3fd66958..2351a59e9 100644 --- a/Client/src/Pages/Monitors/Home/monitorData.jsx +++ b/Client/src/Pages/Monitors/Home/monitorData.jsx @@ -27,30 +27,52 @@ const data = { rows: [], }; +/** + * Builds table data for a list of monitors. + * + * @param {Array} monitors - An array of monitor objects containing information about each monitor. + * @param {boolean} isAdmin - Flag indicating if the current user is an admin. + * @param {Function} navigate - A function to navigate to the monitor detail page. + * @returns {Object} The data structure containing columns and rows for the table. + */ export const buildData = (monitors, isAdmin, navigate) => { const theme = useTheme(); data.rows = monitors.map((monitor, idx) => { let uptimePercentage = ""; + let percentageColor = theme.palette.percentage.uptimeExcellent; + + // Determine uptime percentage and color based on the monitor's uptimePercentage value if (monitor.uptimePercentage !== undefined) { uptimePercentage = monitor.uptimePercentage === 0 - ? "0" - : (monitor.uptimePercentage * 100).toFixed(2); + ? "0" + : (monitor.uptimePercentage * 100).toFixed(2); + + percentageColor = + monitor.uptimePercentage < 0.25 + ? theme.palette.percentage.uptimePoor + : monitor.uptimePercentage < 0.5 + ? theme.palette.percentage.uptimeFair + : monitor.uptimePercentage < 0.75 + ? theme.palette.percentage.uptimeGood + : theme.palette.percentage.uptimeExcellent; } const params = { url: monitor.url, title: monitor.name, percentage: uptimePercentage, - percentageColor: - monitor.status === true - ? theme.palette.success.main - : theme.palette.error.text, - status: monitor.status === true ? "up" : "down", + percentageColor, + status: + monitor.status === undefined + ? "unknown" + : monitor.status === true + ? "up" + : "down", }; - // Reverse checks so latest check is on the right + // Reverse checks so the latest check is on the right const reversedChecks = monitor.checks.slice().reverse(); return { @@ -86,6 +108,3 @@ export const buildData = (monitors, isAdmin, navigate) => { }); return data; }; - - - diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js index 31a23b5b1..6dcde82bb 100644 --- a/Client/src/Utils/NetworkService.js +++ b/Client/src/Utils/NetworkService.js @@ -116,7 +116,7 @@ class NetworkService { * @param {string} [sortOrder] - The order in which to sort the retrieved statistics. * @param {number} [limit] - The maximum number of statistics to retrieve. * @param {string} [dateRange] - The date range for which to retrieve statistics. - * @param {number} [numToDisplay] - The number of statistics to display. + * @param {number} [numToDisplay] - The number of checks to display. * @param {boolean} [normalize] - Whether to normalize the retrieved statistics. * @returns {Promise} The response from the axios GET request. */ diff --git a/Client/src/Utils/Theme/darkTheme.js b/Client/src/Utils/Theme/darkTheme.js index eca445976..1f2b7c85b 100644 --- a/Client/src/Utils/Theme/darkTheme.js +++ b/Client/src/Utils/Theme/darkTheme.js @@ -57,6 +57,12 @@ const darkTheme = createTheme({ bg: "#624711", border: "#e88c30", }, + percentage: { + uptimePoor: "#d32f2f", + uptimeFair: "#e88c30", + uptimeGood: "#ffd600", + uptimeExcellent: "#079455", + }, unresolved: { main: "#4e5ba6", light: "#e2eaf7", bg: "#f2f4f7" }, divider: border.light, other: { diff --git a/Client/src/Utils/Theme/lightTheme.js b/Client/src/Utils/Theme/lightTheme.js index ef38a2abc..4d554c54c 100644 --- a/Client/src/Utils/Theme/lightTheme.js +++ b/Client/src/Utils/Theme/lightTheme.js @@ -54,6 +54,12 @@ const lightTheme = createTheme({ bg: "#ffecbc", border: "#fec84b", }, + percentage: { + uptimePoor: "#d32f2f", + uptimeFair: "#ec8013", + uptimeGood: "#ffb800", + uptimeExcellent: "#079455", + }, unresolved: { main: "#4e5ba6", light: "#e2eaf7", bg: "#f2f4f7" }, divider: border.light, other: { diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index f33ddcf10..0d831f5b6 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -6,12 +6,13 @@ const { editMonitorBodyValidation, getMonitorsByTeamIdQueryValidation, pauseMonitorParamValidation, + getMonitorAggregateStatsParamValidation, + getMonitorAggregateStatsQueryValidation, } = require("../validation/joi"); const sslChecker = require("ssl-checker"); const SERVICE_NAME = "monitorController"; const { errorMessages, successMessages } = require("../utils/messages"); -const { runInNewContext } = require("vm"); /** * Returns all monitors @@ -35,6 +36,44 @@ const getAllMonitors = async (req, res, next) => { } }; +/** + * Returns agregate stats for a monitor + * @async + * @param {Express.Request} req + * @param {Express.Response} res + * @returns {Promise} + * @throws {Error} + */ + +const getMonitorAggregateStats = async (req, res, next) => { + try { + await getMonitorAggregateStatsParamValidation.validateAsync(req.params); + await getMonitorAggregateStatsQueryValidation.validateAsync(req.query); + } catch (error) { + error.status = 422; + error.message = + error.details?.[0]?.message || error.message || "Validation Error"; + next(error); + return; + } + + try { + const { monitorId } = req.params; + const dateRange = req.query.dateRange; + const aggregateStats = await req.db.getMonitorAggregateStats( + monitorId, + dateRange + ); + return res.json({ + success: true, + msg: successMessages.MONTIOR_STATS_BY_ID, + data: aggregateStats, + }); + } catch (error) { + next(error); + } +}; + /** * Returns monitor stats for monitor with matching ID * @async @@ -376,6 +415,7 @@ const pauseMonitor = async (req, res, next) => { module.exports = { getAllMonitors, + getMonitorAggregateStats, getMonitorStatsById, getMonitorCertificate, getMonitorById, diff --git a/Server/db/mongo/MongoDB.js b/Server/db/mongo/MongoDB.js index 912bfb438..6f959f758 100644 --- a/Server/db/mongo/MongoDB.js +++ b/Server/db/mongo/MongoDB.js @@ -64,6 +64,7 @@ const { const { getAllMonitors, + getMonitorAggregateStats, getMonitorStatsById, getMonitorById, getMonitorsByTeamId, @@ -144,6 +145,7 @@ module.exports = { resetPassword, checkSuperadmin, getAllMonitors, + getMonitorAggregateStats, getMonitorStatsById, getMonitorById, getMonitorsByTeamId, diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index 79e8917ea..b913519b6 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -143,6 +143,87 @@ const getStatusBarValues = (monitor, checks) => { } return statusBarValues.reverse(); }; + +/** + * Get aggregate monitor stats for charts + * @async + * @param {Express.Request} req + * @param {Express.Response} res + * @returns {Promise} + * @throws {Error} + */ +const getMonitorAggregateStats = async (monitorId, dateRange) => { + const startDates = { + day: new Date(new Date().setDate(new Date().getDate() - 1)), + week: new Date(new Date().setDate(new Date().getDate() - 7)), + month: new Date(new Date().setMonth(new Date().getMonth() - 1)), + }; + + try { + const startDate = startDates[dateRange]; + if (!startDate) { + throw new Error("Invalid date range specified"); + } + const endDate = new Date(); + + const checks = await Check.find({ + monitorId: monitorId, + createdAt: { + $gte: startDate, + $lte: endDate, + }, + }); + + let groupedChecks; + // Group checks by hour if range is day + if (dateRange === "day") { + groupedChecks = checks.reduce((acc, check) => { + const hour = new Date(check.createdAt).getHours(); + if (!acc[hour]) { + acc[hour] = { hour, checks: [] }; + } + acc[hour].checks.push(check); + return acc; + }, {}); + } + + // Group checks by day if range is week or month + else { + groupedChecks = checks.reduce((acc, check) => { + const day = new Date(check.createdAt).toISOString().split("T")[0]; // Extract the date part + if (!acc[day]) { + acc[day] = { day, checks: [] }; + } + acc[day].checks.push(check); + return acc; + }, {}); + } + + // Map grouped checks to stats + stats = Object.values(groupedChecks).map((group) => { + const totalChecks = group.checks.length; + const totalIncidents = group.checks.filter( + (check) => check.status === false + ).length; + const avgResponseTime = + group.checks.reduce((sum, check) => sum + check.responseTime, 0) / + totalChecks; + + return { + hour: group.hour, + day: group.day, + totalChecks, + totalIncidents, + avgResponseTime, + }; + }); + + return stats; + } catch (error) { + throw error; + } +}; + /** * Get stats by monitor ID * @async @@ -200,6 +281,10 @@ const getMonitorStatsById = async (req) => { ...checksQuery, createdAt: { $gte: filterLookup.day }, }; + const checksQuery7Days = { + ...checksQuery, + createdAt: { $gte: filterLookup.week }, + }; const checksQuery30Days = { ...checksQuery, createdAt: { $gte: filterLookup.month }, @@ -209,15 +294,18 @@ const getMonitorStatsById = async (req) => { createdAt: { $gte: filterLookup.hour }, }; - const [checks24Hours, checks30Days, checks60Mins] = await Promise.all([ - model.find(checksQuery24Hours).sort({ createdAt: sortOrder }), - model.find(checksQuery30Days).sort({ createdAt: sortOrder }), - model.find(checksQuery60Mins).sort({ createdAt: sortOrder }), - ]); + const [checks24Hours, checks7Days, checks30Days, checks60Mins] = + await Promise.all([ + model.find(checksQuery24Hours).sort({ createdAt: sortOrder }), + model.find(checksQuery7Days).sort({ createdAt: sortOrder }), + model.find(checksQuery30Days).sort({ createdAt: sortOrder }), + model.find(checksQuery60Mins).sort({ createdAt: sortOrder }), + ]); // HTTP/PING Specific stats monitorStats.avgResponseTime24hours = getAverageResponseTime24Hours(checks24Hours); + monitorStats.uptime7Days = getUptimePercentage(checks7Days); monitorStats.uptime24Hours = getUptimePercentage(checks24Hours); monitorStats.uptime30Days = getUptimePercentage(checks30Days); monitorStats.statusBar = getStatusBarValues(monitor, checks60Mins); @@ -452,6 +540,7 @@ const editMonitor = async (candidateId, candidateMonitor) => { module.exports = { getAllMonitors, + getMonitorAggregateStats, getMonitorStatsById, getMonitorById, getMonitorsByTeamId, diff --git a/Server/openapi.json b/Server/openapi.json new file mode 100644 index 000000000..23c16521d --- /dev/null +++ b/Server/openapi.json @@ -0,0 +1,835 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "BlueWave Uptime", + "summary": "BlueWave Uptime OpenAPI Specifications", + "description": "BlueWave Uptime is an open source server monitoring application used to track the operational status and performance of servers and websites. It regularly checks whether a server/website is accessible and performs optimally, providing real-time alerts and reports on the monitored services' availability, downtime, and response time.", + "contact": { + "name": "API Support", + "url": "mailto:support@bluewavelabs.ca", + "email": "support@bluewavelabs.ca" + }, + "license": { + "name": "AGPLv3", + "url": "https://github.com/bluewave-labs/bluewave-uptime/tree/HEAD/LICENSE" + }, + "version": "1.0" + }, + "servers": [ + { + "url": "http://localhost:{PORT}/{API_PATH}", + "description": "Local Development Server", + "variables": { + "PORT": { + "description": "API Port", + "enum": ["5000", "3000", "8080"], + "default": "5000" + }, + "API_PATH": { + "description": "API Base Path", + "enum": ["api/v1", "api/v1.1", "api/v2"], + "default": "api/v1" + } + } + } + ], + "tags": [ + { + "name": "auth", + "description": "Authentication" + }, + { + "name": "invite", + "description": "Invite" + }, + { + "name": "monitors", + "description": "Monitors" + }, + { + "name": "checks", + "description": "Checks" + }, + { + "name": "alerts", + "description": "Alerts" + }, + { + "name": "pagespeed", + "description": "Page Speed" + }, + { + "name": "maintenance-window", + "description": "Maintenance window" + }, + { + "name": "healthy", + "description": "Health check" + }, + { + "name": "mail", + "description": "Mail service" + } + ], + "paths": { + "/auth/register": { + "post": { + "tags": ["auth"], + "description": "Register a new user", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["firstName", "lastName", "email", "password", "role", "teamId"], + "properties": { + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "password": { + "type": "string", + "format": "password" + }, + "profileImage": { + "type": "file", + "format": "file" + }, + "role": { + "type": "array", + "enum": [["user"], ["admin"], ["superadmin"]], + "default": ["superadmin"] + }, + "teamId": { + "type": "string", + "format": "uuid" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserSuccessResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/auth/login": { + "post": { + "tags": ["auth"], + "description": "Login with credentials", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["email", "password"], + "properties": { + "email": { + "type": "string", + "format": "email" + }, + "password": { + "type": "string", + "format": "password" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserSuccessResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/auth/user/:userId": { + "put": { + "tags": ["auth"], + "description": "Change user informations", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserSuccessResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "delete": { + "tags": ["auth"], + "description": "Delete user", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserSuccessResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/auth/users/admin": { + "get": { + "tags": ["auth"], + "description": "Checks to see if an admin account exists", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserSuccessResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/auth/users": { + "get": { + "tags": ["auth"], + "description": "Get all users", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserSuccessResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/auth/invite": { + "post": { + "tags": ["auth"], + "description": "Invite people to application", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserSuccessResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/auth/invite/verify": { + "post": { + "tags": ["auth"], + "description": "Verify the invite", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserSuccessResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/auth/recovery/request": { + "post": { + "tags": ["auth"], + "description": "Request a recovery token", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserSuccessResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/auth/recovery/validate": { + "post": { + "tags": ["auth"], + "description": "Validate recovery token", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserSuccessResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/auth/recovery/reset": { + "post": { + "tags": ["auth"], + "description": "Password reset", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserSuccessResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/healthy": { + "get": { + "tags": ["healthy"], + "description": "Health check", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ErrorResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "msg": { + "type": "string" + } + } + }, + "SuccessResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "msg": { + "type": "string" + }, + "data": { + "type": "object" + } + } + }, + "UserSuccessResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "msg": { + "type": "string" + }, + "data": { + "type": "object", + "$ref": "#/components/schemas/UserDataType" + } + } + }, + "MonitorSuccessResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "msg": { + "type": "string" + }, + "data": { + "type": "object", + "$ref": "#/components/schemas/MonitorDataType" + } + } + }, + "CheckDataSuccessResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "msg": { + "type": "string" + }, + "data": { + "type": "object", + "$ref": "#/components/schemas/CheckDataType" + } + } + }, + "AlertDataSuccessResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "msg": { + "type": "string" + }, + "data": { + "type": "object", + "$ref": "#/components/schemas/AlertDataType" + } + } + }, + "UserDataType": { + "type": "object", + "required": [ + "firstname", + "lastname", + "email", + "profilePicUrl", + "isActive", + "isVerified", + "updatedAt", + "createdAt" + ], + "properties": { + "firstname": { + "type": "string" + }, + "lastname": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "profilePicUrl": { + "type": "string" + }, + "isActive": { + "type": "boolean" + }, + "isVerified": { + "type": "boolean" + }, + "updatedAt": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + } + }, + "MonitorDataType": { + "type": "object", + "required": [ + "userId", + "name", + "description", + "url", + "isActive", + "interval", + "updatedAt", + "createdAt" + ], + "properties": { + "userId": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string" + }, + "isActive": { + "type": "boolean" + }, + "interval": { + "type": "integer" + }, + "updatedAt": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + } + }, + "CheckDataType": { + "type": "object", + "required": [ + "monitorId", + "status", + "responseTime", + "statusCode", + "message", + "updatedAt", + "createdAt" + ], + "properties": { + "monitorId": { + "type": "string" + }, + "status": { + "type": "string" + }, + "responseTime": { + "type": "integer" + }, + "statusCode": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + } + }, + "AlertDataType": { + "type": "object", + "required": [ + "checkId", + "monitorId", + "userId", + "status", + "message", + "notifiedStatus", + "acknowledgedStatus", + "updatedAt", + "createdAt" + ], + "properties": { + "checkId": { + "type": "string" + }, + "monitorId": { + "type": "string" + }, + "userId": { + "type": "string" + }, + "status": { + "type": "string" + }, + "message": { + "type": "string" + }, + "notifiedStatus": { + "type": "boolean" + }, + "acknowledgedStatus": { + "type": "boolean" + }, + "updatedAt": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + } + } + } + } +} diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js index ee1c08488..7831f42f7 100644 --- a/Server/routes/monitorRoute.js +++ b/Server/routes/monitorRoute.js @@ -3,6 +3,7 @@ const monitorController = require("../controllers/monitorController"); const { isAllowed } = require("../middleware/isAllowed"); router.get("/", monitorController.getAllMonitors); +router.get("/aggregate/:monitorId", monitorController.getMonitorAggregateStats); router.get("/stats/:monitorId", monitorController.getMonitorStatsById); router.get("/certificate/:monitorId", monitorController.getMonitorCertificate); router.get("/:monitorId", monitorController.getMonitorById); diff --git a/Server/validation/joi.js b/Server/validation/joi.js index ccc8da9f6..629deddf5 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -201,6 +201,13 @@ const pauseMonitorParamValidation = joi.object({ monitorId: joi.string().required(), }); +const getMonitorAggregateStatsParamValidation = joi.object({ + monitorId: joi.string().required(), +}); +const getMonitorAggregateStatsQueryValidation = joi.object({ + dateRange: joi.string().valid("day", "week", "month"), +}); + //**************************************** // Alerts //**************************************** @@ -357,6 +364,8 @@ module.exports = { getMonitorByIdQueryValidation, getMonitorsByTeamIdValidation, getMonitorsByTeamIdQueryValidation, + getMonitorAggregateStatsParamValidation, + getMonitorAggregateStatsQueryValidation, editMonitorBodyValidation, pauseMonitorParamValidation, editUserParamValidation,