diff --git a/Client/src/Pages/Auth/CheckEmail.jsx b/Client/src/Pages/Auth/CheckEmail.jsx
index d6ff54fef..88a4afa1f 100644
--- a/Client/src/Pages/Auth/CheckEmail.jsx
+++ b/Client/src/Pages/Auth/CheckEmail.jsx
@@ -1,15 +1,15 @@
-import "./index.css";
-import background from "../../assets/Images/background_pattern_decorative.png";
-import React, { useEffect, useState } from "react";
-import EmailIcon from "../../assets/icons/email.svg?react";
-import Button from "../../Components/Button";
-import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
-import { Stack, Typography } from "@mui/material";
+import { Box, Stack, Typography } from "@mui/material";
+import { useEffect, useState } from "react";
import { useTheme } from "@emotion/react";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router";
import { createToast } from "../../Utils/toastUtils";
import { forgotPassword } from "../../Features/Auth/authSlice";
+import Button from "../../Components/Button";
+import background from "../../assets/Images/background_pattern_decorative.png";
+import EmailIcon from "../../assets/icons/email.svg?react";
+import Logo from "../../assets/icons/bwu-icon.svg?react";
+import "./index.css";
const CheckEmail = () => {
const theme = useTheme();
@@ -79,28 +79,68 @@ const CheckEmail = () => {
}
};
+ const handleNavigate = () => {
+ sessionStorage.removeItem("email");
+ navigate("/login");
+ };
+
return (
-
+
-
-
+
+
+ Go back to —
+
+ Log In
+
+
+
);
};
diff --git a/Client/src/Pages/Auth/ForgotPassword.jsx b/Client/src/Pages/Auth/ForgotPassword.jsx
index 86db72076..e1f6ae56e 100644
--- a/Client/src/Pages/Auth/ForgotPassword.jsx
+++ b/Client/src/Pages/Auth/ForgotPassword.jsx
@@ -1,18 +1,17 @@
-import "./index.css";
-import background from "../../assets/Images/background_pattern_decorative.png";
-import Logomark from "../../assets/icons/key.svg?react";
-import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
-import { useState } from "react";
-import { credentials } from "../../Validation/validation";
-import { useNavigate } from "react-router-dom";
-import Field from "../../Components/Inputs/Field";
+import { Box, Stack, Typography } from "@mui/material";
+import { useTheme } from "@emotion/react";
import { createToast } from "../../Utils/toastUtils";
import { useDispatch, useSelector } from "react-redux";
import { forgotPassword } from "../../Features/Auth/authSlice";
+import { useEffect, useState } from "react";
+import { credentials } from "../../Validation/validation";
+import { useNavigate } from "react-router-dom";
+import Field from "../../Components/Inputs/Field";
import ButtonSpinner from "../../Components/ButtonSpinner";
-import { Stack, Typography } from "@mui/material";
-import { useTheme } from "@emotion/react";
-import Button from "../../Components/Button";
+import Logo from "../../assets/icons/bwu-icon.svg?react";
+import Key from "../../assets/icons/key.svg?react";
+import background from "../../assets/Images/background_pattern_decorative.png";
+import "./index.css";
const ForgotPassword = () => {
const navigate = useNavigate();
@@ -25,9 +24,10 @@ const ForgotPassword = () => {
email: "",
});
- const idMap = {
- "forgot-password-email-input": "email",
- };
+ useEffect(() => {
+ const email = sessionStorage.getItem("email");
+ email && setForm({ email: sessionStorage.getItem("email") });
+ }, []);
const handleSubmit = async (event) => {
event.preventDefault();
@@ -40,11 +40,9 @@ const ForgotPassword = () => {
error.details && error.details.length > 0
? error.details[0].message
: "Error validating data.";
- setErrors(err);
+ setErrors({ email: err });
createToast({
- variant: "info",
body: err,
- hasIcon: false,
});
} else {
const action = await dispatch(forgotPassword(form));
@@ -71,72 +69,108 @@ const ForgotPassword = () => {
};
const handleChange = (event) => {
- const { value, id } = event.target;
- const name = idMap[id];
- setForm((prev) => ({
- ...prev,
- [name]: value,
- }));
+ const { value } = event.target;
+ setForm({ email: value });
const { error } = credentials.validate(
- { [name]: value },
+ { email: value },
{ abortEarly: false }
);
- setErrors((prev) => {
- const prevErrors = { ...prev };
- if (error) prevErrors[name] = error.details[0].message;
- else delete prevErrors[name];
- return prevErrors;
- });
+ if (error) setErrors({ email: error.details[0].message });
+ else delete errors.email;
+ };
+
+ const handleNavigate = () => {
+ sessionStorage.removeItem("email");
+ navigate("/login");
};
return (
-
+
-
-
+
+
+ Go back to —
+
+ Log In
+
+
+
);
};
diff --git a/Client/src/Pages/Auth/Login.jsx b/Client/src/Pages/Auth/Login.jsx
index eef972ec4..8f378286f 100644
--- a/Client/src/Pages/Auth/Login.jsx
+++ b/Client/src/Pages/Auth/Login.jsx
@@ -154,6 +154,13 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
}
}, []);
+ const handleNavigate = () => {
+ if (form.email !== "" && !errors.email) {
+ sessionStorage.setItem("email", form.email);
+ }
+ navigate("/forgot-password");
+ };
+
return (
<>
@@ -209,7 +216,7 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
component="span"
ml={theme.gap.xs}
sx={{ userSelect: "none" }}
- onClick={() => navigate("/forgot-password")}
+ onClick={handleNavigate}
>
Reset password
diff --git a/Client/src/Pages/Auth/NewPasswordConfirmed.jsx b/Client/src/Pages/Auth/NewPasswordConfirmed.jsx
index f73e5edcf..12a6995f9 100644
--- a/Client/src/Pages/Auth/NewPasswordConfirmed.jsx
+++ b/Client/src/Pages/Auth/NewPasswordConfirmed.jsx
@@ -1,14 +1,14 @@
-import "./index.css";
-import background from "../../assets/Images/background_pattern_decorative.png";
-import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
-import ConfirmIcon from "../../assets/icons/confirm-icon.svg?react";
-import Button from "../../Components/Button";
-import { Stack, Typography } from "@mui/material";
+import { Box, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router";
import { useDispatch } from "react-redux";
import { clearAuthState } from "../../Features/Auth/authSlice";
import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
+import Button from "../../Components/Button";
+import background from "../../assets/Images/background_pattern_decorative.png";
+import ConfirmIcon from "../../assets/icons/confirm-icon.svg?react";
+import Logo from "../../assets/icons/bwu-icon.svg?react";
+import "./index.css";
const NewPasswordConfirmed = () => {
const theme = useTheme();
@@ -22,39 +22,74 @@ const NewPasswordConfirmed = () => {
};
return (
-
+
-
-
+
+
+ Go back to —
+
+ Log In
+
+
+
);
};
diff --git a/Client/src/Pages/Auth/Register/Register.jsx b/Client/src/Pages/Auth/Register/Register.jsx
index 77b3dd2fc..855cc3c54 100644
--- a/Client/src/Pages/Auth/Register/Register.jsx
+++ b/Client/src/Pages/Auth/Register/Register.jsx
@@ -107,18 +107,16 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
/>
@@ -267,21 +265,19 @@ const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => {
/>
-
+
{
const navigate = useNavigate();
@@ -105,100 +104,138 @@ const SetNewPassword = () => {
};
return (
-
+
-
-
+
+
+ Go back to —
+ navigate("/login")}
+ sx={{ userSelect: "none" }}
+ >
+ Log In
+
+
+
);
};
diff --git a/Client/src/Pages/Auth/index.css b/Client/src/Pages/Auth/index.css
index 577f27799..f7f2c211e 100644
--- a/Client/src/Pages/Auth/index.css
+++ b/Client/src/Pages/Auth/index.css
@@ -1,80 +1,3 @@
-/* ////// */
-/* SHARED */
-/* ////// */
-.set-new-password-page,
-.password-confirmed-page,
-.forgot-password-page,
-.check-email-page {
- display: flex;
- justify-content: center;
- height: var(--env-var-height-1);
-}
-.set-new-password-page h1.MuiTypography-root,
-.password-confirmed-page h1.MuiTypography-root,
-.forgot-password-page h1.MuiTypography-root,
-.check-email-page h1.MuiTypography-root {
- font-size: var(--env-var-font-size-large);
- color: var(--env-var-color-1);
- font-weight: 600;
-}
-.set-new-password-page p.MuiTypography-root,
-.set-new-password-page button,
-.password-confirmed-page p.MuiTypography-root,
-.password-confirmed-page button,
-.forgot-password-page p.MuiTypography-root,
-.forgot-password-page button,
-.check-email-page p.MuiTypography-root,
-.check-email-page button,
-.check-email-page span.MuiTypography-root {
- font-size: var(--env-var-font-size-medium);
-}
-.set-new-password-page button:not(.MuiIconButton-root),
-.password-confirmed-page button:not(.MuiIconButton-root),
-.forgot-password-page button:not(.MuiIconButton-root),
-.check-email-page button:not(.MuiIconButton-root) {
- height: 34px;
- border-radius: var(--env-var-radius-2);
- line-height: 0;
-}
-
-.set-new-password-form,
-.password-confirmed-form,
-.forgot-password-form,
-.check-email-form {
- margin-top: 65px;
- width: var(--env-var-width-2);
-}
-.set-new-password-page p.MuiTypography-root,
-.password-confirmed-page p.MuiTypography-root,
-.forgot-password-page p.MuiTypography-root,
-.check-email-page p.MuiTypography-root {
- color: var(--env-var-color-2);
-}
-
-.set-new-password-page button:not(.MuiIconButton-root) svg,
-.password-confirmed-page button:not(.MuiIconButton-root) svg,
-.forgot-password-page button svg,
-.check-email-page button svg {
- margin-right: 5px;
-}
-
-.set-new-password-page
- .MuiFormControl-root:has(#register-password-input)
- + span.MuiTypography-root,
-.password-confirmed-page
- .MuiFormControl-root:has(#register-password-input)
- + span.MuiTypography-root {
- display: none;
- visibility: hidden;
-}
-
-/* /////////// */
-/* Check Email */
-/* /////////// */
-.check-email-page span.MuiTypography-root {
- font-weight: 600;
-}
-
/* AUTH */
.auth {
height: 100vh;
@@ -106,13 +29,13 @@
cursor: pointer;
transition: opacity 300ms ease-in;
}
-.auth p > span {
+.auth p > span:not(.email-sent-to) {
text-decoration: underline;
text-underline-offset: 2px;
cursor: pointer;
transition: all 200ms;
}
-.auth p > span:hover {
+.auth p > span:not(.email-sent-to):hover {
color: var(--env-var-color-2);
text-underline-offset: 4px;
}
@@ -135,6 +58,13 @@
.auth .check span.MuiTypography-root {
font-size: var(--env-var-font-size-medium);
}
+.auth form + form {
+ margin: var(--env-var-spacing-1-plus) 0;
+}
+.auth .email-sent-to {
+ font-weight: 600;
+ cursor: default;
+}
.auth > .MuiStack-root:nth-of-type(1) {
height: var(--env-var-nav-bar-height);
@@ -144,3 +74,10 @@
width: 100%;
max-width: 800px;
}
+
+.forgot-password-page h1,
+.check-email-page h1,
+.password-confirmed-page h1,
+.set-new-password-page h1 {
+ font-size: var(--env-var-font-size-large-plus);
+}
diff --git a/Client/src/Pages/Incidents/IncidentTable/index.jsx b/Client/src/Pages/Incidents/IncidentTable/index.jsx
index 95230bb72..44a30decc 100644
--- a/Client/src/Pages/Incidents/IncidentTable/index.jsx
+++ b/Client/src/Pages/Incidents/IncidentTable/index.jsx
@@ -9,6 +9,8 @@ import {
Pagination,
PaginationItem,
Paper,
+ Typography,
+ Box,
} from "@mui/material";
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
@@ -105,41 +107,55 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
return (
<>
-
-
-
-
- Monitor Name
- Status
- Date & Time
- Message
-
-
-
- {checks.map((check) => {
- const status = check.status === true ? "up" : "down";
-
- return (
-
- {monitors[check.monitorId]?.name}
-
-
-
-
- {new Date(check.createdAt).toLocaleString()}
-
- {check.statusCode}
+ {checks?.length === 0 && selectedMonitor === "0" ? (
+
+ No incidents recorded yet.
+
+ ) : checks?.length === 0 ? (
+
+
+ The monitor you have selected has no recorded incidents yet.
+
+
+ ) : (
+ <>
+
+
+
+
+ Monitor Name
+ Status
+ Date & Time
+ Message
- );
- })}
-
-
-
- {paginationComponent}
+
+
+ {checks.map((check) => {
+ const status = check.status === true ? "up" : "down";
+
+ return (
+
+ {monitors[check.monitorId]?.name}
+
+
+
+
+ {new Date(check.createdAt).toLocaleString()}
+
+ {check.statusCode}
+
+ );
+ })}
+
+
+
+ {paginationComponent}
+ >
+ )}
>
);
};
diff --git a/Client/src/Pages/Incidents/index.css b/Client/src/Pages/Incidents/index.css
index 1f176d470..286552d22 100644
--- a/Client/src/Pages/Incidents/index.css
+++ b/Client/src/Pages/Incidents/index.css
@@ -17,6 +17,12 @@
.incidents .MuiPagination-root {
background-color: var(--env-var-color-8);
}
+.incidents > .MuiBox-root {
+ background-color: var(--env-var-color-8);
+ border: solid 1px var(--env-var-color-6);
+ border-radius: var(--env-var-radius-1);
+ padding: 60px;
+}
/* remove later */
.incidents .MuiSelect-select {
diff --git a/Client/src/Pages/Monitors/Details/index.jsx b/Client/src/Pages/Monitors/Details/index.jsx
index 4168880d5..918077a91 100644
--- a/Client/src/Pages/Monitors/Details/index.jsx
+++ b/Client/src/Pages/Monitors/Details/index.jsx
@@ -5,7 +5,6 @@ import { useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import axiosInstance from "../../../Utils/axiosConfig";
import MonitorDetailsAreaChart from "../../../Components/Charts/MonitorDetailsAreaChart";
-import { StatusLabel } from "../../../Components/Label";
import ButtonGroup from "@mui/material/ButtonGroup";
import Button from "../../../Components/Button";
import WestRoundedIcon from "@mui/icons-material/WestRounded";
@@ -119,7 +118,7 @@ const DetailsPage = () => {
const fetchMonitor = useCallback(async () => {
try {
const res = await axiosInstance.get(
- `/monitors/${monitorId}?sortOrder=asc&filter=${dateRange}&numToDisplay=50&normalize=true`,
+ `/monitors/stats/${monitorId}?filter=${dateRange}&numToDisplay=50&normalize=true`,
{
headers: {
Authorization: `Bearer ${authToken}`,
@@ -139,67 +138,6 @@ const DetailsPage = () => {
const theme = useTheme();
- /**
- * Function to calculate uptime duration based on the most recent check.
- * @param {Array} checks Array of check objects.
- * @returns {number} Uptime duration in ms.
- */
-
- // TODO: This can be done more efficiently by iteratting backwards
- // and breaking when the first down check is found, calculate current time - downtime
- const calculateUptimeDuration = (checks) => {
- if (!checks || checks.length === 0) {
- return 0;
- }
-
- const latestCheck = new Date(checks[checks.length - 1].createdAt);
- let latestDownCheck = 0;
-
- // Checks are ordered oldest -> newest
- // So we iterate backwards and find the first down check
- for (let i = checks.length - 1; i >= 0; i--) {
- if (checks[i].status === false) {
- latestDownCheck = new Date(checks[i].createdAt);
- break;
- }
- }
-
- // If no down check is found, uptime is from the first check to now
- if (latestDownCheck === 0) {
- return Date.now() - new Date(checks[0].createdAt);
- }
-
- // Otherwise the uptime is from the last check to the last down check
- return latestCheck - latestDownCheck;
- };
-
- /**
- * Helper function to get duration since last check
- * @param {Array} checks Array of check objects.
- * @returns {number} Timestamp of the most recent check.
- */
- const getLastChecked = (checks) => {
- if (!checks || checks.length === 0) {
- return 0; // Handle case when no checks are available
- }
- // Data is sorted oldest -> newest, so last check is the most recent
- return new Date() - new Date(checks[checks.length - 1].createdAt);
- };
-
- /**
- * Helper function to count incidents (checks with status === false).
- * @param {Array} checks Array of check objects.
- * @returns {number} Number of incidents.
- */
- const countIncidents = (checks) => {
- if (!checks || checks.length === 0) {
- return 0; // Handle case when no checks are available
- }
- return checks.reduce((acc, check) => {
- return check.status === false ? (acc += 1) : acc;
- }, 0);
- };
-
let loading = Object.keys(monitor).length === 0;
return (
@@ -243,7 +181,7 @@ const DetailsPage = () => {
{" "}
Checking every {formatDurationRounded(monitor?.interval)}.
Last time checked{" "}
- {formatDurationRounded(getLastChecked(monitor?.checks))} ago.
+ {formatDurationRounded(monitor?.lastChecked)} ago.
{
-
+
diff --git a/Client/src/Pages/PageSpeed/Details/index.jsx b/Client/src/Pages/PageSpeed/Details/index.jsx
index 45a031da1..04e08d122 100644
--- a/Client/src/Pages/PageSpeed/Details/index.jsx
+++ b/Client/src/Pages/PageSpeed/Details/index.jsx
@@ -5,7 +5,11 @@ import { useEffect, useState } from "react";
import { useTheme } from "@emotion/react";
import { useNavigate, useParams } from "react-router-dom";
import { useSelector } from "react-redux";
-import { formatDate, formatDurationRounded } from "../../../Utils/timeUtils";
+import {
+ formatDate,
+ formatDuration,
+ formatDurationRounded,
+} from "../../../Utils/timeUtils";
import { getLastChecked } from "../../../Utils/monitorUtils";
import axiosInstance from "../../../Utils/axiosConfig";
import Button from "../../../Components/Button";
@@ -185,7 +189,7 @@ const PageSpeedDetails = () => {
const fetchMonitor = async () => {
try {
const res = await axiosInstance.get(
- `/monitors/${monitorId}?sortOrder=desc`,
+ `/monitors/stats/${monitorId}?sortOrder=desc&limit=50`,
{
headers: {
Authorization: `Bearer ${authToken}`,
@@ -369,14 +373,13 @@ const PageSpeedDetails = () => {
title="Last checked"
value={
<>
- {formatDate(getLastChecked(monitor?.checks, false))}{" "}
+ {formatDuration(monitor?.lastChecked)}{" "}
- ({formatDurationRounded(getLastChecked(monitor?.checks))}{" "}
- ago)
+ ago
>
}
@@ -386,17 +389,13 @@ const PageSpeedDetails = () => {
title="Checks since"
value={
<>
- {formatDate(new Date(monitor?.createdAt))}{" "}
+ {formatDuration(monitor?.uptimeDuration)}{" "}
- (
- {formatDurationRounded(
- new Date() - new Date(monitor?.createdAt)
- )}{" "}
- ago)
+ ago
>
}
@@ -409,9 +408,7 @@ const PageSpeedDetails = () => {
Score history
-
+
Performance report
diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js
index 0f89e3371..d4a77fe4d 100644
--- a/Server/controllers/authController.js
+++ b/Server/controllers/authController.js
@@ -126,11 +126,12 @@ const loginController = async (req, res, next) => {
return;
}
try {
+ const { email, password } = req.body;
// Check if user exists
- const user = await req.db.getUserByEmail(req, res);
+ const user = await req.db.getUserByEmail(email);
// Compare password
- const match = await user.comparePassword(req.body.password);
+ const match = await user.comparePassword(password);
if (match !== true) {
throw new Error(errorMessages.AUTH_INCORRECT_PASSWORD);
}
@@ -167,7 +168,7 @@ const logoutController = async (req, res, next) => {
try {
// Get user
const { email } = req.body;
- const userToLogout = await req.db.getUserByEmail({ body: { email } }, res);
+ const userToLogout = await req.db.getUserByEmail(email);
userToLogout.authToken = null;
await userToLogout.save();
@@ -216,7 +217,7 @@ const userEditController = async (req, res, next) => {
// Add user email to body for DB operation
req.body.email = email;
// Get user
- const user = await req.db.getUserByEmail(req, res);
+ const user = await req.db.getUserByEmail(email);
// Compare passwords
const match = await user.comparePassword(req.body.password);
// If not a match, throw a 403
@@ -350,7 +351,8 @@ const recoveryRequestController = async (req, res, next) => {
}
try {
- const user = await req.db.getUserByEmail(req, res);
+ const { email } = req.body;
+ const user = await req.db.getUserByEmail(email);
if (user) {
const recoveryToken = await req.db.requestRecoveryToken(req, res);
const name = user.firstName;
@@ -462,41 +464,16 @@ const deleteUserController = async (req, res, next) => {
const decodedToken = jwt.decode(token);
const { _id, email } = decodedToken;
- // TODO fix this hack
- const decodedTokenCastedAsRequest = {
- query: {},
- params: {
- userId: _id,
- },
- body: {
- email,
- },
- };
-
- // Validate user
- try {
- await deleteUserParamValidation.validateAsync(
- decodedTokenCastedAsRequest.body
- );
- } catch (error) {
- error.status = 422;
- error.service = SERVICE_NAME;
- error.message =
- error.details?.[0]?.message || error.message || "Validation Error";
- next(error);
- return;
- }
-
// Check if the user exists
- const user = await req.db.getUserByEmail(decodedTokenCastedAsRequest);
+ const user = await req.db.getUserByEmail(email);
if (!user) {
throw new Error(errorMessages.DB_USER_NOT_FOUND);
}
// 1. Find all the monitors associated with the user id
- const monitors = await req.db.getMonitorsByUserId(
- decodedTokenCastedAsRequest
- );
+ const monitors = await req.db.getMonitorsByUserId({
+ params: { userId: _id },
+ });
if (monitors) {
//2. Remove all jobs, delete checks and alerts
@@ -514,7 +491,7 @@ const deleteUserController = async (req, res, next) => {
await req.db.deleteMonitorsByUserId(user._id);
// 4. Delete the user by id
- await req.db.deleteUser(decodedTokenCastedAsRequest);
+ await req.db.deleteUser(user._id);
return res.status(200).json({
success: true,
diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js
index d45b300f8..b8aa24c25 100644
--- a/Server/controllers/monitorController.js
+++ b/Server/controllers/monitorController.js
@@ -33,6 +33,38 @@ const getAllMonitors = async (req, res, next) => {
}
};
+/**
+ * Returns monitor stats for monitor with matching ID
+ * @async
+ * @param {Express.Request} req
+ * @param {Express.Response} res
+ * @returns {Promise}
+ * @throws {Error}
+ */
+const getMonitorStatsById = async (req, res, next) => {
+ try {
+ //Validation
+ } catch (error) {
+ error.status = 422;
+ error.message =
+ error.details?.[0]?.message || error.message || "Validation Error";
+ next(error);
+ return;
+ }
+
+ try {
+ const monitorStats = await req.db.getMonitorStatsById(req);
+ return res.json({
+ success: true,
+ msg: successMessages.MONTIOR_STATS_BY_ID,
+ data: monitorStats,
+ });
+ } catch (error) {
+ error.service = SERVICE_NAME;
+ next(error);
+ }
+};
+
/**
* Returns monitor with matching ID
* @async
@@ -269,6 +301,7 @@ const editMonitor = async (req, res, next) => {
module.exports = {
getAllMonitors,
+ getMonitorStatsById,
getMonitorById,
getMonitorsByUserId,
createMonitor,
diff --git a/Server/db/mongo/MongoDB.js b/Server/db/mongo/MongoDB.js
index fa84cd98a..9c1005c32 100644
--- a/Server/db/mongo/MongoDB.js
+++ b/Server/db/mongo/MongoDB.js
@@ -64,6 +64,7 @@ const {
const {
getAllMonitors,
+ getMonitorStatsById,
getMonitorById,
getMonitorsByUserId,
createMonitor,
@@ -142,6 +143,7 @@ module.exports = {
resetPassword,
checkAdmin,
getAllMonitors,
+ getMonitorStatsById,
getMonitorById,
getMonitorsByUserId,
createMonitor,
diff --git a/Server/db/mongo/modules/checkModule.js b/Server/db/mongo/modules/checkModule.js
index 92dc83f84..782e5f64d 100644
--- a/Server/db/mongo/modules/checkModule.js
+++ b/Server/db/mongo/modules/checkModule.js
@@ -33,7 +33,6 @@ const getChecksCount = async (req) => {
const monitorId = req.params.monitorId;
const dateRange = req.query.dateRange;
const filter = req.query.filter;
- console.log(filter);
// Build query
const checksQuery = { monitorId: monitorId };
// Filter checks by "day", "week", or "month"
diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js
index 34255b2e8..9a0ed7463 100644
--- a/Server/db/mongo/modules/monitorModule.js
+++ b/Server/db/mongo/modules/monitorModule.js
@@ -22,6 +22,232 @@ const getAllMonitors = async (req, res) => {
}
};
+/**
+ * Function to calculate uptime duration based on the most recent check.
+ * @param {Array} checks Array of check objects.
+ * @returns {number} Uptime duration in ms.
+ */
+const calculateUptimeDuration = (checks) => {
+ if (!checks || checks.length === 0) {
+ return 0;
+ }
+
+ const latestCheck = new Date(checks[0].createdAt);
+ let latestDownCheck = 0;
+
+ for (let i = 0; i < checks.length; i++) {
+ if (checks[i].status === false) {
+ latestDownCheck = new Date(checks[i].createdAt);
+ break;
+ }
+ }
+
+ // If no down check is found, uptime is from the last check to now
+ if (latestDownCheck === 0) {
+ return Date.now() - new Date(checks[checks.length - 1].createdAt);
+ }
+
+ // Otherwise the uptime is from the last check to the last down check
+ return latestCheck - latestDownCheck;
+};
+
+/**
+ * Helper function to get duration since last check
+ * @param {Array} checks Array of check objects.
+ * @returns {number} Timestamp of the most recent check.
+ */
+const getLastChecked = (checks) => {
+ if (!checks || checks.length === 0) {
+ return 0; // Handle case when no checks are available
+ }
+ // Data is sorted newest->oldest, so last check is the most recent
+ return new Date() - new Date(checks[0].createdAt);
+};
+
+/**
+ * Helper function to get latestResponseTime
+ * @param {Array} checks Array of check objects.
+ * @returns {number} Timestamp of the most recent check.
+ */
+const getLatestResponseTime = (checks) => {
+ if (!checks || checks.length === 0) {
+ return 0;
+ }
+ return checks[0].responseTime;
+};
+
+/**
+ * Helper function to get average 24h response time
+ * @param {Array} checks Array of check objects.
+ * @returns {number} Timestamp of the most recent check.
+ */
+const getAverageResponseTime24Hours = (checks) => {
+ if (!checks || checks.length === 0) {
+ return 0;
+ }
+ const aggResponseTime = checks.reduce((sum, check) => {
+ return sum + check.responseTime;
+ }, 0);
+ return aggResponseTime / checks.length;
+};
+
+/**
+ * Helper function to get precentage 24h uptime
+ * @param {Array} checks Array of check objects.
+ * @returns {number} Timestamp of the most recent check.
+ */
+
+const getUptimePercentage = (checks) => {
+ if (!checks || checks.length === 0) {
+ return 0;
+ }
+ const upCount = checks.reduce((count, check) => {
+ return check.status === true ? count + 1 : count;
+ }, 0);
+ return (upCount / checks.length) * 100;
+};
+
+/**
+ * Helper function to get all incidents
+ * @param {Array} checks Array of check objects.
+ * @returns {number} Timestamp of the most recent check.
+ */
+
+const getIncidents = (checks) => {
+ if (!checks || checks.length === 0) {
+ return 0; // Handle case when no checks are available
+ }
+ return checks.reduce((acc, check) => {
+ return check.status === false ? (acc += 1) : acc;
+ }, 0);
+};
+/**
+ * Helper function to get all incidents
+ * @param {Array} checks Array of check objects.
+ * @returns {Array} Array of booleans representing up/down.
+ */
+const getStatusBarValues = (checks) => {
+ return checks.map((check) => {
+ return check.status;
+ });
+};
+/**
+ * Get stats by monitor ID
+ * @async
+ * @param {Express.Request} req
+ * @param {Express.Response} res
+ * @returns {Promise}
+ * @throws {Error}
+ */
+const getMonitorStatsById = async (req) => {
+ const filterLookup = {
+ hour: new Date(new Date().getTime() - 60 * 60 * 1000),
+ 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 { monitorId } = req.params;
+ let { status, limit, sortOrder, filter, numToDisplay, normalize } =
+ req.query;
+
+ // This effectively removes limit, returning all checks
+ if (limit === undefined) limit = 0;
+
+ // Default sort order is newest -> oldest
+ sortOrder = sortOrder === "asc" ? 1 : -1;
+
+ // Get monitor
+ const monitor = await Monitor.findById(monitorId);
+
+ let model =
+ monitor.type === "http" || monitor.type === "ping"
+ ? Check
+ : PageSpeedCheck;
+
+ const monitorStats = {
+ ...monitor.toObject(),
+ };
+ // Get all checks to calculate stats
+ const checksQuery = { monitorId: monitor._id };
+ // Get all checks
+ const checksAll = await model.find(checksQuery).sort({
+ createdAt: sortOrder,
+ });
+
+ if (monitor.type === "http" || monitor.type === "ping") {
+ // Get checks in 24 hours
+ checksQuery.createdAt = { $gte: filterLookup.day };
+ const checks24Hours = await model.find(checksQuery).sort({
+ createdAt: sortOrder,
+ });
+ // Get checks in 30 days
+ checksQuery.createdAt = { $gte: filterLookup.month };
+ const checks30Days = await model.find(checksQuery).sort({
+ createdAt: sortOrder,
+ });
+ // Get checks in 60 mins
+ checksQuery.createdAt = { $gte: filterLookup.hour };
+ const checks60Mins = await model
+ .find(checksQuery)
+ .sort({ createdAt: sortOrder });
+
+ // HTTP/PING Specific stats
+ monitorStats.avgResponseTime24hours =
+ getAverageResponseTime24Hours(checks24Hours);
+ monitorStats.uptime24Hours = getUptimePercentage(checks24Hours);
+ monitorStats.uptime30Days = getUptimePercentage(checks30Days);
+ monitorStats.statusBar = getStatusBarValues(checks60Mins);
+ }
+
+ //Get checks for dateRange
+
+ if (status !== undefined) {
+ checksQuery.status = status;
+ }
+
+ // Filter checks by "day", "week", or "month"
+ if (filter !== undefined) {
+ checksQuery.createdAt = { $gte: filterLookup[filter] };
+ }
+
+ let dateRangeChecks = await model
+ .find(checksQuery)
+ .sort({
+ createdAt: sortOrder,
+ })
+ .limit(limit);
+
+ // If more than numToDisplay checks, pick every nth check
+ if (
+ numToDisplay !== undefined &&
+ dateRangeChecks &&
+ dateRangeChecks.length > numToDisplay
+ ) {
+ const n = Math.ceil(dateRangeChecks.length / numToDisplay);
+ dateRangeChecks = dateRangeChecks.filter((_, index) => index % n === 0);
+ }
+
+ // Normalize checks if requested
+ if (normalize !== undefined) {
+ dateRangeChecks = NormalizeData(dateRangeChecks, 1, 100);
+ }
+
+ // Add common stats
+ const incidents = getIncidents(dateRangeChecks);
+ monitorStats.uptimeDuration = caluclteUptimeDuration(checksAll);
+ monitorStats.lastChecked = getLastChecked(checksAll);
+ monitorStats.latestResponseTime = getLatestResponseTime(checksAll);
+ monitorStats.incidents = incidents;
+ monitorStats.checks = dateRangeChecks;
+
+ return monitorStats;
+ } catch (error) {
+ console.log(error);
+ }
+};
+
/**
* Get a monitor by ID
* @async
@@ -263,6 +489,7 @@ const editMonitor = async (req, res) => {
module.exports = {
getAllMonitors,
+ getMonitorStatsById,
getMonitorById,
getMonitorsByUserId,
createMonitor,
diff --git a/Server/db/mongo/modules/userModule.js b/Server/db/mongo/modules/userModule.js
index d626bcb9a..3dacfd943 100644
--- a/Server/db/mongo/modules/userModule.js
+++ b/Server/db/mongo/modules/userModule.js
@@ -51,11 +51,11 @@ const insertUser = async (req, res) => {
* @returns {Promise}
* @throws {Error}
*/
-const getUserByEmail = async (req, res) => {
+const getUserByEmail = async (email) => {
try {
// Need the password to be able to compare, removed .select()
// We can strip the hash before returing the user
- const user = await UserModel.findOne({ email: req.body.email }).select(
+ const user = await UserModel.findOne({ email: email }).select(
"-profileImage"
);
if (user) {
@@ -126,8 +126,7 @@ const updateUser = async (req, res) => {
* @returns {Promise}
* @throws {Error}
*/
-const deleteUser = async (req, res) => {
- const userId = req.params.userId;
+const deleteUser = async (userId) => {
try {
const deletedUser = await UserModel.findByIdAndDelete(userId);
if (!deletedUser) {
diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js
index 2622181a3..ba585affb 100644
--- a/Server/routes/monitorRoute.js
+++ b/Server/routes/monitorRoute.js
@@ -4,6 +4,7 @@ const { verifyOwnership } = require("../middleware/verifyOwnership");
const Monitor = require("../models/Monitor");
router.get("/", monitorController.getAllMonitors);
+router.get("/stats/:monitorId", monitorController.getMonitorStatsById);
router.get("/:monitorId", monitorController.getMonitorById);
router.get("/user/:userId", monitorController.getMonitorsByUserId);
diff --git a/Server/utils/messages.js b/Server/utils/messages.js
index 40d7205e5..4e56ec5b7 100644
--- a/Server/utils/messages.js
+++ b/Server/utils/messages.js
@@ -71,6 +71,7 @@ const successMessages = {
//Monitor Controller
MONITOR_GET_ALL: "Got all monitors successfully",
+ MONTIOR_STATS_BY_ID: "Got monitor stats by Id successfully",
MONTIOR_GET_BY_ID: "Got monitor by Id successfully",
MONITOR_GET_BY_USER_ID: (userId) => `Got monitor for ${userId} successfully"`,
MONITOR_CREATE: "Monitor created successfully",