diff --git a/Client/package-lock.json b/Client/package-lock.json
index 2a7b9b7e1..89a508703 100644
--- a/Client/package-lock.json
+++ b/Client/package-lock.json
@@ -18,7 +18,7 @@
"@mui/x-data-grid": "7.3.2",
"@mui/x-date-pickers": "7.3.2",
"@reduxjs/toolkit": "2.2.5",
- "axios": "1.7.2",
+ "axios": "1.7.4",
"chart.js": "^4.4.3",
"dayjs": "1.11.11",
"joi": "17.13.1",
@@ -29,7 +29,7 @@
"react-router": "^6.23.0",
"react-router-dom": "^6.23.1",
"react-toastify": "^10.0.5",
- "recharts": "2.12.7",
+ "recharts": "2.13.0-alpha.4",
"redux-persist": "6.0.0",
"vite-plugin-svgr": "^4.2.0"
},
@@ -2660,7 +2660,7 @@
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
"license": "MIT",
"dependencies": {
- "follow-redirects": "^1.15.6",
+ "follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
@@ -5509,15 +5509,15 @@
}
},
"node_modules/recharts": {
- "version": "2.12.7",
- "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.7.tgz",
- "integrity": "sha512-hlLJMhPQfv4/3NBSAyq3gzGg4h2v69RJh6KU7b3pXYNNAELs9kEoXOjbkxdXpALqKBoVmVptGfLpxdaVYqjmXQ==",
+ "version": "2.13.0-alpha.4",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.13.0-alpha.4.tgz",
+ "integrity": "sha512-K9naL6F7pEcDYJE6yFQASSCQecSLPP0JagnvQ9hPtA/aHgsxsnIOjouLP5yrFZehxzfCkV5TEORr7/uNtSr7Qw==",
"license": "MIT",
"dependencies": {
"clsx": "^2.0.0",
"eventemitter3": "^4.0.1",
"lodash": "^4.17.21",
- "react-is": "^16.10.2",
+ "react-is": "^18.3.1",
"react-smooth": "^4.0.0",
"recharts-scale": "^0.4.4",
"tiny-invariant": "^1.3.1",
@@ -5540,12 +5540,6 @@
"decimal.js-light": "^2.4.1"
}
},
- "node_modules/recharts/node_modules/react-is": {
- "version": "16.13.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "license": "MIT"
- },
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
diff --git a/Client/package.json b/Client/package.json
index f711bff9a..769e1f4a6 100644
--- a/Client/package.json
+++ b/Client/package.json
@@ -20,7 +20,7 @@
"@mui/x-data-grid": "7.3.2",
"@mui/x-date-pickers": "7.3.2",
"@reduxjs/toolkit": "2.2.5",
- "axios": "1.7.2",
+ "axios": "1.7.4",
"chart.js": "^4.4.3",
"dayjs": "1.11.11",
"joi": "17.13.1",
@@ -31,7 +31,7 @@
"react-router": "^6.23.0",
"react-router-dom": "^6.23.1",
"react-toastify": "^10.0.5",
- "recharts": "2.12.7",
+ "recharts": "2.13.0-alpha.4",
"redux-persist": "6.0.0",
"vite-plugin-svgr": "^4.2.0"
},
diff --git a/Client/src/Components/Avatar/index.jsx b/Client/src/Components/Avatar/index.jsx
index 4f4351508..2a9f70a08 100644
--- a/Client/src/Components/Avatar/index.jsx
+++ b/Client/src/Components/Avatar/index.jsx
@@ -3,6 +3,27 @@ import PropTypes from "prop-types";
import { useSelector } from "react-redux";
import { useEffect, useState } from "react";
+/**
+ * Generates a color based on the input string.
+ * @param {string} string - The input string to generate the color from.
+ * @returns {string}
+ */
+const stringToColor = (string) => {
+ let hash = 0;
+ let i;
+ for (i = 0; i < string.length; i += 1) {
+ hash = string.charCodeAt(i) + ((hash << 5) - hash);
+ }
+
+ let color = "#";
+ for (i = 0; i < 3; i += 1) {
+ const value = (hash >> (i * 8)) & 0xff;
+ color += `00${value.toString(16)}`.slice(-2);
+ }
+
+ return color;
+};
+
/**
* @component
* @param {Object} props
@@ -18,7 +39,7 @@ import { useEffect, useState } from "react";
const Avatar = ({ src, small, sx }) => {
const { user } = useSelector((state) => state.auth);
- const style = small ? { width: 25, height: 25 } : { width: 64, height: 64 };
+ const style = small ? { width: 32, height: 32 } : { width: 64, height: 64 };
const border = small ? 1 : 3;
const [image, setImage] = useState();
@@ -35,8 +56,9 @@ const Avatar = ({ src, small, sx }) => {
src ? src : user?.avatarImage ? image : "/static/images/avatar/2.jpg"
}
sx={{
- fontSize: small ? "13px" : "22px",
+ fontSize: small ? "16px" : "22px",
fontWeight: 400,
+ backgroundColor: stringToColor(`${user?.firstName} ${user?.lastName}`),
display: "inline-flex",
"&::before": {
content: `""`,
@@ -52,7 +74,8 @@ const Avatar = ({ src, small, sx }) => {
...sx,
}}
>
- {user.firstName?.charAt(0)}{user.lastName?.charAt(0)}
+ {user.firstName?.charAt(0)}
+ {user.lastName?.charAt(0)}
);
};
diff --git a/Client/src/Components/BasicTable/index.css b/Client/src/Components/BasicTable/index.css
index fd1f75b53..14adc440c 100644
--- a/Client/src/Components/BasicTable/index.css
+++ b/Client/src/Components/BasicTable/index.css
@@ -72,7 +72,7 @@
}
.MuiPaper-root + .MuiPagination-root {
- margin-top: 35px;
+ margin-top: 24px;
border: solid 1px var(--env-var-color-16);
border-radius: var(--env-var-radius-1);
padding: var(--env-var-spacing-1-plus) var(--env-var-spacing-2);
diff --git a/Client/src/Components/Charts/MonitorDetails60MinChart/index.jsx b/Client/src/Components/Charts/MonitorDetails60MinChart/index.jsx
new file mode 100644
index 000000000..f7fc0d3d8
--- /dev/null
+++ b/Client/src/Components/Charts/MonitorDetails60MinChart/index.jsx
@@ -0,0 +1,50 @@
+import { BarChart, Bar, Cell, ReferenceLine, Label } from "recharts";
+
+import PropTypes from "prop-types";
+
+const MonitorDetails60MinChart = ({ data }) => {
+ const labelStyle = {
+ fontSize: "10px",
+ fill: "#475467",
+ };
+
+ const color = {
+ true: "var(--env-var-color-23)",
+ false: "var(--env-var-color-24)",
+ undefined: "var(--env-var-color-33)",
+ };
+ return (
+
+
+ {data.map((check, index) => (
+ |
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+MonitorDetails60MinChart.propTypes = {
+ data: PropTypes.array.isRequired,
+};
+
+export default MonitorDetails60MinChart;
diff --git a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx
index 91657f542..c37e467f6 100644
--- a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx
+++ b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx
@@ -28,15 +28,6 @@ const CustomToolTip = ({ active, payload, label }) => {
};
const MonitorDetailsAreaChart = ({ checks }) => {
- // SQUASH ERROR, NOT PERMANENT SOLUTION
- const error = console.error;
- console.error = (...args) => {
- if (/defaultProps/.test(args[0])) return;
- error(...args);
- };
-
- // END SQUASH ERROR, NOT PERMANENT SOLUTION
-
const formatDate = (timestamp) => {
const date = new Date(timestamp);
return date.toLocaleTimeString("en-US", {
diff --git a/Client/src/Components/Check/Check.jsx b/Client/src/Components/Check/Check.jsx
index 1b246566f..645e0b5e2 100644
--- a/Client/src/Components/Check/Check.jsx
+++ b/Client/src/Components/Check/Check.jsx
@@ -46,7 +46,7 @@ const Check = ({ text, variant = "info", outlined = false }) => {
};
Check.propTypes = {
- text: PropTypes.string.isRequired,
+ text: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
variant: PropTypes.oneOf(["info", "error", "success"]),
outlined: PropTypes.bool,
};
diff --git a/Client/src/Components/Check/check.css b/Client/src/Components/Check/check.css
index cb24be466..f1432d2bc 100644
--- a/Client/src/Components/Check/check.css
+++ b/Client/src/Components/Check/check.css
@@ -1,4 +1,4 @@
-.check span.MuiTypography-root {
+.check > span.MuiTypography-root {
font-size: var(--env-var-font-size-small-plus);
color: var(--env-var-color-2);
opacity: 0.8;
diff --git a/Client/src/Components/Fallback/index.jsx b/Client/src/Components/Fallback/index.jsx
index 164753104..976440f7d 100644
--- a/Client/src/Components/Fallback/index.jsx
+++ b/Client/src/Components/Fallback/index.jsx
@@ -1,7 +1,8 @@
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
-import { Stack, Typography } from "@mui/material";
+import { Box, Stack, Typography } from "@mui/material";
import Skeleton from "../../assets/Images/create-placeholder.svg?react";
+import background from "../../assets/Images/background_pattern_decorative.png";
import Button from "../Button";
import Check from "../Check/Check";
import { useNavigate } from "react-router-dom";
@@ -28,8 +29,12 @@ const Fallback = ({ title, checks, link = "/" }) => {
alignItems="center"
gap={theme.gap.xl}
>
-
-
+
+
+
A {title} is used to:
diff --git a/Client/src/Components/NavBar/index.css b/Client/src/Components/NavBar/index.css
deleted file mode 100644
index fcedc53a2..000000000
--- a/Client/src/Components/NavBar/index.css
+++ /dev/null
@@ -1,34 +0,0 @@
-/* NavBar Component Styles*/
-.MuiToolbar-root {
- min-height: var(--env-var-nav-bar-height) !important;
-}
-
-.icon-button-toggle-title {
- font-size: var(--env-var-font-size-medium);
- color: var(--env-var-color-5);
-}
-
-.icon-button-toggle-pic {
- width: 10px;
- height: 5px;
-}
-
-#icon-button {
- -webkit-transition: none;
- transition: none;
- outline: none;
- box-shadow: none;
- background-color: transparent;
-}
-
-#menu-appbar svg {
- width: 16px;
- height: 16px;
- color: var(--env-var-color-25);
-}
-
-#bw-uptime-logo-dashboard {
- width: fit-content;
- height: 16px;
- margin-left: 15px;
-}
diff --git a/Client/src/Components/NavBar/index.jsx b/Client/src/Components/NavBar/index.jsx
deleted file mode 100644
index 95123d975..000000000
--- a/Client/src/Components/NavBar/index.jsx
+++ /dev/null
@@ -1,203 +0,0 @@
-import "./index.css";
-import { cloneElement, useState } from "react";
-import AppBar from "@mui/material/AppBar";
-import Box from "@mui/material/Box";
-import Toolbar from "@mui/material/Toolbar";
-import IconButton from "@mui/material/IconButton";
-import Typography from "@mui/material/Typography";
-import Menu from "@mui/material/Menu";
-import Avatar from "../Avatar";
-import Tooltip from "@mui/material/Tooltip";
-import MenuItem from "@mui/material/MenuItem";
-import { useTheme } from "@mui/material/styles";
-import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
-import { clearAuthState } from "../../Features/Auth/authSlice";
-import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
-
-import { useDispatch, useSelector } from "react-redux";
-import { useNavigate } from "react-router-dom";
-import LockSvg from "../../assets/icons/lock.svg?react";
-import UserSvg from "../../assets/icons/user.svg?react";
-import TeamSvg from "../../assets/icons/user-two.svg?react";
-import LogoutSvg from "../../assets/icons/logout.svg?react";
-import { Stack, useScrollTrigger } from "@mui/material";
-import axiosIntance from "../../Utils/axiosConfig";
-import axios from "axios";
-
-const icons = {
- Profile: ,
- Team: ,
- Password: ,
- Logout: ,
-};
-
-function AddBorderOnScroll(props) {
- const { children, window } = props;
- const trigger = useScrollTrigger({
- target: window ? window() : undefined,
- disableHysteresis: true,
- threshold: 0,
- });
-
- return (
-
- {children}
-
- );
-}
-
-/**
- * NavBar component
- *
- * A responsive navigation bar component with a user menu.
- *
- * @component
- * @example
- * return (
- *
- * )
- */
-function NavBar() {
- const theme = useTheme();
- const [anchorElUser, setAnchorElUser] = useState(null);
- const dispatch = useDispatch();
- const navigate = useNavigate();
- const authState = useSelector((state) => state.auth);
-
- // Initialize settings and update based on user role
- let settings = ["Profile", "Password", "Team", "Logout"];
- if (authState.user?.role && !authState.user.role.includes("admin")) {
- settings = ["Profile", "Password", "Logout"];
- }
-
- /**
- * Handles opening the user menu.
- *
- * @param {React.MouseEvent} event - The event triggered by clicking the user menu button.
- */
- const handleOpenUserMenu = (event) => {
- setAnchorElUser(event.currentTarget);
- };
-
- /**
- * Handles logging out the user
- *
- */
- const logout = async () => {
- // Clear auth state
- dispatch(clearAuthState());
- dispatch(clearUptimeMonitorState());
- // Make request to BE to remove JWT from user
- await axiosIntance.post(
- "/auth/logout",
- { email: authState.user.email },
- {
- headers: {
- Authorization: `Bearer ${authState.authToken}`,
- "Content-Type": "application/json",
- },
- }
- );
- navigate("/login");
- };
-
- /**
- * Handles closing the user menu.
- */
- const handleCloseUserMenu = (setting) => {
- setAnchorElUser(null);
- switch (setting) {
- case "Profile":
- navigate("/account/profile");
- break;
- case "Team":
- navigate("/account/team");
- break;
- case "Password":
- navigate("/account/password");
- break;
- case "Logout":
- logout();
- break;
- default:
- break;
- }
- };
-
- return (
-
-
-
-
-
-
-
- {authState.user?.firstName} {authState.user?.lastName}
-
-
-
-
-
-
-
-
- );
-}
-
-export default NavBar;
diff --git a/Client/src/Components/Sidebar/index.css b/Client/src/Components/Sidebar/index.css
index 6fa9d2134..4840043c3 100644
--- a/Client/src/Components/Sidebar/index.css
+++ b/Client/src/Components/Sidebar/index.css
@@ -18,11 +18,14 @@ aside span.MuiTypography-root {
color: var(--env-var-color-5);
line-height: 1;
}
-aside .MuiStack-root:nth-last-child(2) {
+aside .MuiStack-root:nth-last-child(3) {
margin-top: auto;
+}
+aside .MuiStack-root:last-child,
+aside .MuiStack-root:nth-last-child(3) {
position: relative;
}
-aside .MuiStack-root:nth-last-child(2):before {
+aside .MuiStack-root:last-child:before {
content: "";
position: absolute;
top: -10px;
@@ -31,5 +34,35 @@ aside .MuiStack-root:nth-last-child(2):before {
border-top: solid 1px var(--env-var-color-6);
}
aside .MuiStack-root:last-child {
- margin-bottom: 10px;
+ margin-top: 15px;
+}
+
+.sidebar-menu {
+ margin-top: -20px;
+}
+.sidebar-menu .MuiPaper-root {
+ box-shadow: var(--env-var-shadow-1);
+ border: solid 1px var(--env-var-color-6);
+ border-radius: var(--env-var-radius-1);
+ gap: 1px;
+}
+.sidebar-menu .MuiList-root {
+ min-width: 100px;
+ width: 150px;
+ padding-bottom: 0;
+}
+.sidebar-menu li.MuiButtonBase-root:last-child {
+ border-top: solid 1px var(--env-var-color-6);
+ padding: 12px 16px;
+}
+.sidebar-menu li.MuiButtonBase-root {
+ min-height: fit-content;
+}
+.sidebar-menu .MuiList-root svg {
+ width: 16px;
+ height: 16px;
+}
+.sidebar-menu span {
+ font-size: var(--env-var-font-size-medium);
+ color: var(--env-var-color-2);
}
diff --git a/Client/src/Components/Sidebar/index.jsx b/Client/src/Components/Sidebar/index.jsx
index 3b2a32394..5a5b1d980 100644
--- a/Client/src/Components/Sidebar/index.jsx
+++ b/Client/src/Components/Sidebar/index.jsx
@@ -1,7 +1,17 @@
-import { Stack, Typography } from "@mui/material";
+import { useState } from "react";
+import { Menu, MenuItem, Stack, Tooltip, Typography } from "@mui/material";
import { useLocation, useNavigate } from "react-router";
import { useTheme } from "@emotion/react";
-
+import { useDispatch, useSelector } from "react-redux";
+import { clearAuthState } from "../../Features/Auth/authSlice";
+import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
+import { createToast } from "../../Utils/toastUtils";
+import axiosInstance from "../../Utils/axiosConfig";
+import Avatar from "../Avatar";
+import LockSvg from "../../assets/icons/lock.svg?react";
+import UserSvg from "../../assets/icons/user.svg?react";
+import TeamSvg from "../../assets/icons/user-two.svg?react";
+import LogoutSvg from "../../assets/icons/logout.svg?react";
import BWULogo from "../../assets/Images/bwl-logo.svg?react";
import Support from "../../assets/icons/support.svg?react";
import StatusPages from "../../assets/icons/status-pages.svg?react";
@@ -11,16 +21,10 @@ import Incidents from "../../assets/icons/incidents.svg?react";
import Integrations from "../../assets/icons/integrations.svg?react";
import PageSpeed from "../../assets/icons/page-speed.svg?react";
import Settings from "../../assets/icons/settings.svg?react";
+import Arrow from "../../assets/icons/down-arrow.svg?react";
import "./index.css";
-/**
- * @component
- * Sidebar component serves as a sidebar containing a menu.
- *
- * @returns {JSX.Element} The JSX element representing the Sidebar component.
- */
-
const menu = [
{ name: "Monitors", path: "monitors", icon: },
{ name: "Incidents", path: "incidents", icon: },
@@ -32,10 +36,94 @@ const menu = [
{ name: "Settings", path: "settings", icon: },
];
+const icons = {
+ Profile: ,
+ Team: ,
+ Password: ,
+ Logout: ,
+};
+
+/**
+ * @component
+ * Sidebar component serves as a sidebar containing a menu.
+ *
+ * @returns {JSX.Element} The JSX element representing the Sidebar component.
+ */
+
function Sidebar() {
const theme = useTheme();
const navigate = useNavigate();
const location = useLocation();
+ const dispatch = useDispatch();
+ const [anchorElUser, setAnchorElUser] = useState(null);
+ const authState = useSelector((state) => state.auth);
+
+ // Initialize settings and update based on user role
+ let settings = ["Profile", "Password", "Team", "Logout"];
+ if (authState.user?.role && !authState.user.role.includes("admin")) {
+ settings = ["Profile", "Password", "Logout"];
+ }
+
+ /**
+ * Handles opening the user menu.
+ *
+ * @param {React.MouseEvent} event - The event triggered by clicking the user menu button.
+ */
+ const handleOpenUserMenu = (event) => {
+ setAnchorElUser(event.currentTarget);
+ };
+
+ /**
+ * Handles logging out the user
+ *
+ */
+ const logout = async () => {
+ try {
+ // Make request to BE to remove JWT from user
+ await axiosInstance.post(
+ "/auth/logout",
+ { email: authState.user.email },
+ {
+ headers: {
+ Authorization: `Bearer ${authState.authToken}`,
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ // Clear auth state
+ dispatch(clearAuthState());
+ dispatch(clearUptimeMonitorState());
+ navigate("/login");
+ } catch (error) {
+ createToast({
+ body: error.message,
+ });
+ }
+ };
+
+ /**
+ * Handles closing the user menu.
+ */
+ const handleCloseUserMenu = (setting) => {
+ setAnchorElUser(null);
+ switch (setting) {
+ case "Profile":
+ navigate("/account/profile");
+ break;
+ case "Team":
+ navigate("/account/team");
+ break;
+ case "Password":
+ navigate("/account/password");
+ break;
+ case "Logout":
+ logout();
+ break;
+ default:
+ break;
+ }
+ };
return (
@@ -48,6 +136,8 @@ function Sidebar() {
key={item.path}
direction="row"
alignItems="center"
+ py={theme.gap.small}
+ px={theme.gap.medium}
gap={theme.gap.small}
borderRadius={`${theme.shape.borderRadius}px`}
onClick={() =>
@@ -59,12 +149,66 @@ function Sidebar() {
)
: navigate(`/${item.path}`)
}
- sx={{ p: `${theme.gap.small} ${theme.gap.medium}` }}
>
{item.icon}
{item.name}
))}
+
+
+
+
+ {authState.user?.firstName} {authState.user?.lastName}
+
+
+
+
+
);
}
diff --git a/Client/src/Features/Auth/authSlice.js b/Client/src/Features/Auth/authSlice.js
index a30c295d9..6da315fc4 100644
--- a/Client/src/Features/Auth/authSlice.js
+++ b/Client/src/Features/Auth/authSlice.js
@@ -20,7 +20,11 @@ export const register = createAsyncThunk(
if (error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
- return thunkApi.rejectWithValue(error.message);
+ const payload = {
+ status: false,
+ msg: error.message ? error.message : "Unknown error",
+ };
+ return thunkApi.rejectWithValue(payload);
}
}
);
@@ -33,7 +37,11 @@ export const login = createAsyncThunk("auth/login", async (form, thunkApi) => {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
- return thunkApi.rejectWithValue(error.message);
+ const payload = {
+ status: false,
+ msg: error.message ? error.message : "Unknown error",
+ };
+ return thunkApi.rejectWithValue(payload);
}
});
@@ -72,7 +80,11 @@ export const update = createAsyncThunk(
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
- return thunkApi.rejectWithValue(error.message);
+ const payload = {
+ status: false,
+ msg: error.message ? error.message : "Unknown error",
+ };
+ return thunkApi.rejectWithValue(payload);
}
}
);
@@ -91,7 +103,11 @@ export const deleteUser = createAsyncThunk(
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
- return thunkApi.rejectWithValue(error.message);
+ const payload = {
+ status: false,
+ msg: error.message ? error.message : "Unknown error",
+ };
+ return thunkApi.rejectWithValue(payload);
}
}
);
@@ -106,7 +122,11 @@ export const forgotPassword = createAsyncThunk(
if (error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
- return thunkApi.rejectWithValue(error.message);
+ const payload = {
+ status: false,
+ msg: error.message ? error.message : "Unknown error",
+ };
+ return thunkApi.rejectWithValue(payload);
}
}
);
@@ -128,7 +148,11 @@ export const setNewPassword = createAsyncThunk(
if (error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
- return thunkApi.rejectWithValue(error.message);
+ const payload = {
+ status: false,
+ msg: error.message ? error.message : "Unknown error",
+ };
+ return thunkApi.rejectWithValue(payload);
}
}
);
diff --git a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js
index 4377edb35..6af825a37 100644
--- a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js
+++ b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js
@@ -25,7 +25,11 @@ export const createPageSpeed = createAsyncThunk(
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
- return thunkApi.rejectWithValue(error.message);
+ const payload = {
+ status: false,
+ msg: error.message ? error.message : "Unknown error",
+ };
+ return thunkApi.rejectWithValue(payload);
}
}
);
@@ -40,7 +44,11 @@ export const getPageSpeedMonitors = createAsyncThunk(
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
- return thunkApi.rejectWithValue(error.message);
+ const payload = {
+ status: false,
+ msg: error.message ? error.message : "Unknown error",
+ };
+ return thunkApi.rejectWithValue(payload);
}
}
);
@@ -63,7 +71,11 @@ export const getPageSpeedByUserId = createAsyncThunk(
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
- return thunkApi.rejectWithValue(error.message);
+ const payload = {
+ status: false,
+ msg: error.message ? error.message : "Unknown error",
+ };
+ return thunkApi.rejectWithValue(payload);
}
}
);
@@ -94,7 +106,11 @@ export const updatePageSpeed = createAsyncThunk(
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
- return thunkApi.rejectWithValue(error.message);
+ const payload = {
+ status: false,
+ msg: error.message ? error.message : "Unknown error",
+ };
+ return thunkApi.rejectWithValue(payload);
}
}
);
@@ -115,7 +131,11 @@ export const deletePageSpeed = createAsyncThunk(
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
- return thunkApi.rejectWithValue(error.message);
+ const payload = {
+ status: false,
+ msg: error.message ? error.message : "Unknown error",
+ };
+ return thunkApi.rejectWithValue(payload);
}
}
);
diff --git a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js
index e69e6d41d..27d616e1e 100644
--- a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js
+++ b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js
@@ -25,7 +25,11 @@ export const createUptimeMonitor = createAsyncThunk(
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
- return thunkApi.rejectWithValue(error.message);
+ const payload = {
+ status: false,
+ msg: error.message ? error.message : "Unknown error",
+ };
+ return thunkApi.rejectWithValue(payload);
}
}
);
@@ -40,7 +44,11 @@ export const getUptimeMonitors = createAsyncThunk(
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
- return thunkApi.rejectWithValue(error.message);
+ const payload = {
+ status: false,
+ msg: error.message ? error.message : "Unknown error",
+ };
+ return thunkApi.rejectWithValue(payload);
}
}
);
@@ -63,7 +71,11 @@ export const getUptimeMonitorsByUserId = createAsyncThunk(
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
- return thunkApi.rejectWithValue(error.message);
+ const payload = {
+ status: false,
+ msg: error.message ? error.message : "Unknown error",
+ };
+ return thunkApi.rejectWithValue(payload);
}
}
);
@@ -94,7 +106,11 @@ export const updateUptimeMonitor = createAsyncThunk(
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
- return thunkApi.rejectWithValue(error.message);
+ const payload = {
+ status: false,
+ msg: error.message ? error.message : "Unknown error",
+ };
+ return thunkApi.rejectWithValue(payload);
}
}
);
@@ -115,7 +131,11 @@ export const deleteUptimeMonitor = createAsyncThunk(
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
- return thunkApi.rejectWithValue(error.message);
+ const payload = {
+ status: false,
+ msg: error.message ? error.message : "Unknown error",
+ };
+ return thunkApi.rejectWithValue(payload);
}
}
);
diff --git a/Client/src/Layouts/HomeLayout/index.css b/Client/src/Layouts/HomeLayout/index.css
index 048700db6..2798bd292 100644
--- a/Client/src/Layouts/HomeLayout/index.css
+++ b/Client/src/Layouts/HomeLayout/index.css
@@ -1,55 +1,59 @@
-.home-layout {
- display: grid;
- position: relative;
- grid-template-areas:
- "aside header"
- "aside main";
- grid-template-columns: var(--env-var-side-bar-width) auto;
- grid-template-rows: var(--env-var-nav-bar-height) auto;
+#root:has(.home-layout) {
background-color: var(--env-var-color-30);
+}
+
+.home-layout {
+ display: flex;
+ position: relative;
+ gap: var(--env-var-spacing-2);
min-height: 100vh;
+ max-width: 1400px;
+ margin: 0 auto;
+ padding: var(--env-var-spacing-2);
}
.home-layout aside {
- grid-area: aside;
- position: fixed;
- top: 0;
- height: 100%;
- width: var(--env-var-side-bar-width);
+ position: sticky;
+ top: var(--env-var-spacing-2);
+ left: 0;
+
+ height: calc(100vh - var(--env-var-spacing-2) * 2);
+ max-width: var(--env-var-side-bar-width);
+ flex: 1;
+
+ border: 1px solid var(--color-border-0);
+ border-radius: var(--env-var-radius-1);
background-color: var(--env-var-color-8);
- border-right: 1px solid var(--color-border-0);
+
padding: var(--env-var-spacing-1) var(--env-var-spacing-1-plus);
}
-.home-layout header {
- grid-area: header;
- background-color: var(--env-var-color-30);
-}
+
.home-layout > div {
+ min-height: calc(100vh - var(--env-var-spacing-2) * 2);
+ flex: 1;
+}
+
+.home-layout > div:has(> [class*="fallback__"]) {
+ border: solid 1px var(--env-var-color-6);
+ border-radius: var(--env-var-radius-1);
+ border-style: dashed;
+ background-color: var(--env-var-color-8);
+ overflow: hidden;
+ position: relative;
+}
+.home-layout > div:has(> [class*="fallback__"]) .background-pattern-svg {
+ position: absolute;
+ top: 0;
+ left: 50%;
+ transform: translate(-50%, -33%);
+ z-index: 0;
+
+ width: 100%;
+ max-width: 800px;
height: 100%;
- grid-area: main;
- padding: var(--env-var-spacing-2) calc(var(--env-var-spacing-4) * 2);
- padding-bottom: var(--env-var-spacing-4);
- max-width: 100%;
-}
-.home-layout > div:not(:has([class*="fallback"])) {
- max-width: 1500px;
-}
+ max-height: 800px;
-@media (max-width: 600px) {
- .home-layout {
- grid-template-areas:
- "header header"
- "main main";
- grid-template-columns: 1fr;
- grid-template-rows: 64px auto;
- }
-
- aside {
- display: none;
- visibility: hidden;
- }
-
- .home-layout > div {
- padding: var(--env-var-spacing-1);
- }
+ background-position: center;
+ background-size: cover;
+ background-repeat: no-repeat;
}
diff --git a/Client/src/Layouts/HomeLayout/index.jsx b/Client/src/Layouts/HomeLayout/index.jsx
index 007fb7d05..81cb2fd9c 100644
--- a/Client/src/Layouts/HomeLayout/index.jsx
+++ b/Client/src/Layouts/HomeLayout/index.jsx
@@ -1,4 +1,3 @@
-import NavBar from "../../Components/NavBar";
import Sidebar from "../../Components/Sidebar";
import { Outlet } from "react-router";
import { Box } from "@mui/material";
@@ -9,7 +8,6 @@ const HomeLayout = () => {
return (
-
);
diff --git a/Client/src/Pages/Account/index.jsx b/Client/src/Pages/Account/index.jsx
index db08ec3a2..82f37a969 100644
--- a/Client/src/Pages/Account/index.jsx
+++ b/Client/src/Pages/Account/index.jsx
@@ -29,7 +29,7 @@ const Account = ({ open = "profile" }) => {
if (!user.role.includes("admin")) tabList = ["Profile", "Password"];
return (
-
+
{
const theme = useTheme();
@@ -79,28 +79,71 @@ 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..c51417c91 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,111 @@ 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..ad5fcdbce 100644
--- a/Client/src/Pages/Auth/Login.jsx
+++ b/Client/src/Pages/Auth/Login.jsx
@@ -28,7 +28,11 @@ const LandingPage = ({ onContinue }) => {
return (
<>
-
+
Log In
We are pleased to see you again!
@@ -82,7 +86,7 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
return (
<>
-
+
Log In
Enter your email address
@@ -154,9 +158,16 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
}
}, []);
+ const handleNavigate = () => {
+ if (form.email !== "" && !errors.email) {
+ sessionStorage.setItem("email", form.email);
+ }
+ navigate("/forgot-password");
+ };
+
return (
<>
-
+
Log In
Enter your password
@@ -209,7 +220,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
@@ -329,10 +340,9 @@ const Login = () => {
return (
-
{
maxWidth={600}
flex={1}
justifyContent="center"
- p={theme.gap.xl}
- pb={theme.gap.triplexl}
+ px={{ xs: theme.gap.large, lg: theme.gap.xl }}
+ pb={theme.gap.xl}
mx="auto"
sx={{
"& > .MuiStack-root": {
diff --git a/Client/src/Pages/Auth/NewPasswordConfirmed.jsx b/Client/src/Pages/Auth/NewPasswordConfirmed.jsx
index f73e5edcf..be6aeb9b5 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,77 @@ 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..30b439f16 100644
--- a/Client/src/Pages/Auth/Register/Register.jsx
+++ b/Client/src/Pages/Auth/Register/Register.jsx
@@ -31,7 +31,11 @@ const LandingPage = ({ isAdmin, onSignup }) => {
return (
<>
-
+
Sign Up
@@ -87,7 +91,7 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
return (
<>
-
+
Sign Up
Enter your personal details
@@ -107,18 +111,16 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
/>
@@ -129,7 +131,6 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
img={}
onClick={onBack}
sx={{
- mb: theme.gap.medium,
px: theme.gap.ml,
"& svg.MuiSvgIcon-root": {
mr: theme.gap.xs,
@@ -173,7 +174,7 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
return (
<>
-
+
Sign Up
Enter your email address
@@ -202,7 +203,6 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
img={}
onClick={onBack}
sx={{
- mb: theme.gap.medium,
px: theme.gap.ml,
"& svg.MuiSvgIcon-root": {
mr: theme.gap.xs,
@@ -246,7 +246,7 @@ const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => {
return (
<>
-
+
Sign Up
Create your password
@@ -267,23 +267,29 @@ const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => {
/>
-
+
+ Must be at least 8
+ characters long
+ >
+ }
variant={
errors?.password === "Password is required"
? "error"
@@ -295,7 +301,12 @@ const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => {
}
/>
+ Must contain one
+ special character and a number
+ >
+ }
variant={
errors?.password === "Password is required"
? "error"
@@ -309,7 +320,14 @@ const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => {
}
/>
+
+ Must contain at least
+ {" "}
+ one upper and lower character
+ >
+ }
variant={
errors?.password === "Password is required"
? "error"
@@ -330,7 +348,6 @@ const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => {
img={}
onClick={onBack}
sx={{
- mb: theme.gap.medium,
px: theme.gap.ml,
"& svg.MuiSvgIcon-root": {
mr: theme.gap.xs,
@@ -513,10 +530,9 @@ const Register = ({ isAdmin }) => {
return (
-
{
maxWidth={600}
flex={1}
justifyContent="center"
- p={theme.gap.xl}
- pb={theme.gap.triplexl}
+ px={{ xs: theme.gap.large, lg: theme.gap.xl }}
+ pb={theme.gap.xl}
mx="auto"
sx={{
"& > .MuiStack-root": {
diff --git a/Client/src/Pages/Auth/SetNewPassword.jsx b/Client/src/Pages/Auth/SetNewPassword.jsx
index ce95fb7a5..fab448ffa 100644
--- a/Client/src/Pages/Auth/SetNewPassword.jsx
+++ b/Client/src/Pages/Auth/SetNewPassword.jsx
@@ -1,20 +1,19 @@
-import "./index.css";
-import background from "../../assets/Images/background_pattern_decorative.png";
-import LockIcon from "../../assets/icons/lock-button-icon.svg?react";
-import Check from "../../Components/Check/Check";
-import ButtonSpinner from "../../Components/ButtonSpinner";
-import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
+import { useDispatch, useSelector } from "react-redux";
+import { setNewPassword } from "../../Features/Auth/authSlice";
+import { createToast } from "../../Utils/toastUtils";
+import { Box, Stack, Typography } from "@mui/material";
+import { useTheme } from "@emotion/react";
import { useParams } from "react-router-dom";
import { useState } from "react";
import { credentials } from "../../Validation/validation";
import { useNavigate } from "react-router-dom";
+import Check from "../../Components/Check/Check";
+import ButtonSpinner from "../../Components/ButtonSpinner";
import Field from "../../Components/Inputs/Field";
-import { useDispatch, useSelector } from "react-redux";
-import { setNewPassword } from "../../Features/Auth/authSlice";
-import { createToast } from "../../Utils/toastUtils";
-import { Stack, Typography } from "@mui/material";
-import { useTheme } from "@emotion/react";
-import Button from "../../Components/Button";
+import LockIcon from "../../assets/icons/lock-button-icon.svg?react";
+import background from "../../assets/Images/background_pattern_decorative.png";
+import Logo from "../../assets/icons/bwu-icon.svg?react";
+import "./index.css";
const SetNewPassword = () => {
const navigate = useNavigate();
@@ -105,100 +104,158 @@ 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..979ae191c 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;
@@ -85,10 +8,8 @@
color: var(--env-var-color-3);
}
.auth button:not(.MuiIconButton-root),
-.auth .field h3.MuiTypography-root,
-.auth .field input,
-.auth .field .input-error,
.auth p,
+.auth .field input,
.auth span {
font-size: var(--env-var-font-size-medium-plus);
}
@@ -106,13 +27,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;
}
@@ -132,15 +53,80 @@
width: 24px;
height: 24px;
}
+.auth .field h3.MuiTypography-root,
+.auth .field .input-error,
.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) {
+.auth > .MuiStack-root:nth-of-type(2) {
height: var(--env-var-nav-bar-height);
}
.auth .background-pattern-svg {
- top: 14%;
+ position: absolute;
+ top: 0;
+ left: 50%;
+ transform: translate(-50%, -30%);
+ z-index: -1;
+
width: 100%;
max-width: 800px;
+ height: 100%;
+ max-height: 800px;
+
+ background-position: center;
+ background-size: cover;
+ background-repeat: no-repeat;
+}
+
+.forgot-password-page h1,
+.check-email-page h1,
+.password-confirmed-page h1,
+.set-new-password-page h1 {
+ font-size: 22px;
+}
+
+@media (max-width: 800px) {
+ .auth h1 {
+ font-size: var(--env-var-font-size-large-plus);
+ }
+ .auth button:not(.MuiIconButton-root),
+ .auth .field input,
+ .auth p,
+ .auth span {
+ font-size: var(--env-var-font-size-medium);
+ }
+ .auth button:not(.MuiIconButton-root),
+ .auth .field .MuiInputBase-root:has(input) {
+ height: 36px;
+ }
+ .auth .check span.MuiTypography-root,
+ .auth .field .input-error,
+ .auth .field h3.MuiTypography-root {
+ font-size: var(--env-var-font-size-small-plus);
+ }
+ .auth .check span > span {
+ display: none;
+ }
+ .auth form + form {
+ margin: var(--env-var-spacing-1-plus) 0;
+ }
+ .auth .background-pattern-svg {
+ max-width: 750px;
+ max-height: 750px;
+ }
+
+ .forgot-password-page h1,
+ .check-email-page h1,
+ .password-confirmed-page h1,
+ .set-new-password-page h1 {
+ font-size: 18px;
+ }
}
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/Incidents/index.jsx b/Client/src/Pages/Incidents/index.jsx
index 515f8ec0c..f9d53f7c0 100644
--- a/Client/src/Pages/Incidents/index.jsx
+++ b/Client/src/Pages/Incidents/index.jsx
@@ -78,7 +78,7 @@ const Incidents = () => {
};
return (
-
+
{loading ? (
) : (
diff --git a/Client/src/Pages/Integrations/index.jsx b/Client/src/Pages/Integrations/index.jsx
index f7d693948..278754fea 100644
--- a/Client/src/Pages/Integrations/index.jsx
+++ b/Client/src/Pages/Integrations/index.jsx
@@ -108,7 +108,7 @@ const Integrations = () => {
];
return (
-
+
Integrations
Connect BlueWave Uptime to your favorite service.
diff --git a/Client/src/Pages/Monitors/Configure/index.css b/Client/src/Pages/Monitors/Configure/index.css
index a594348ab..ac9cd8a0d 100644
--- a/Client/src/Pages/Monitors/Configure/index.css
+++ b/Client/src/Pages/Monitors/Configure/index.css
@@ -75,7 +75,7 @@
.configure-monitor-form {
display: flex;
flex-direction: column;
- gap: var(--env-var-spacing-4);
+ gap: var(--env-var-spacing-2);
}
body:has(.configure-monitor) .select-dropdown .MuiMenuItem-root,
diff --git a/Client/src/Pages/Monitors/CreateMonitor/index.css b/Client/src/Pages/Monitors/CreateMonitor/index.css
index 1eb542a44..3fa38f14a 100644
--- a/Client/src/Pages/Monitors/CreateMonitor/index.css
+++ b/Client/src/Pages/Monitors/CreateMonitor/index.css
@@ -59,7 +59,7 @@
.create-monitor-form {
display: flex;
flex-direction: column;
- gap: var(--env-var-spacing-4);
+ gap: var(--env-var-spacing-2);
}
.create-monitor .MuiStack-root .MuiButtonGroup-root button {
diff --git a/Client/src/Pages/Monitors/Details/index.css b/Client/src/Pages/Monitors/Details/index.css
index 359a002bc..127b7377a 100644
--- a/Client/src/Pages/Monitors/Details/index.css
+++ b/Client/src/Pages/Monitors/Details/index.css
@@ -3,12 +3,10 @@
color: var(--env-var-color-1);
font-weight: 600;
}
-.monitor-details h2.MuiTypography-root,
-.monitor-details h6.MuiTypography-root {
+.monitor-details h2.MuiTypography-root {
font-size: var(--env-var-font-size-large);
}
-.monitor-details h2.MuiTypography-root,
-.monitor-details h4.MuiTypography-root {
+.monitor-details h2.MuiTypography-root{
font-weight: 600;
}
.monitor-details h2.MuiTypography-root,
@@ -35,15 +33,21 @@
}
.monitor-details .stat-box {
- flex: 1;
+ flex: 1 20%;
min-width: 100px;
border: solid 1px;
border-radius: var(--env-var-radius-1);
- padding: var(--env-var-spacing-1) 18px;
+ padding: var(--env-var-spacing-1) 16px;
background-color: var(--env-var-color-8);
}
-.monitor-details .stat-box h6.MuiTypography-root {
- margin-bottom: var(--env-var-spacing-1);
+.monitor-details h6.MuiTypography-root {
+ font-size: var(--env-var-font-size-medium-plus);
+}
+.monitor-details h6.MuiTypography-root span.MuiTypography-root {
+ font-style: italic;
+ font-size: var(--env-var-font-size-medium);
+ color: var(--env-var-color-5);
+ opacity: 0.7;
}
.monitor-details button.MuiButtonBase-root,
.monitor-details .stat-box {
diff --git a/Client/src/Pages/Monitors/Details/index.jsx b/Client/src/Pages/Monitors/Details/index.jsx
index 4168880d5..15cfd2c82 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";
@@ -18,20 +17,23 @@ import {
formatDurationRounded,
} from "../../../Utils/timeUtils";
import "./index.css";
+import MonitorDetails60MinChart from "../../../Components/Charts/MonitorDetails60MinChart";
const StatBox = ({ title, value }) => {
return (
-
+
{title}
- {value}
+
+ {value}
+
);
};
StatBox.propTypes = {
- title: PropTypes.string,
+ title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
@@ -114,12 +116,13 @@ const DetailsPage = () => {
const { monitorId } = useParams();
const { authToken } = useSelector((state) => state.auth);
const [dateRange, setDateRange] = useState("day");
+ const [certificateExpiry, setCertificateExpiry] = useState("N/A");
const navigate = useNavigate();
const fetchMonitor = useCallback(async () => {
try {
const res = await axiosInstance.get(
- `/monitors/${monitorId}?sortOrder=asc&filter=${dateRange}&numToDisplay=50&normalize=true`,
+ `/monitors/stats/${monitorId}?dateRange=${dateRange}&numToDisplay=50&normalize=true`,
{
headers: {
Authorization: `Bearer ${authToken}`,
@@ -137,71 +140,23 @@ const DetailsPage = () => {
fetchMonitor();
}, [fetchMonitor]);
+ useEffect(() => {
+ const fetchCertificate = async () => {
+ const res = await axiosInstance.get(
+ `/monitors/certificate/${monitorId}`,
+ {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ },
+ }
+ );
+ setCertificateExpiry(res.data.data.certificateDate);
+ };
+ fetchCertificate();
+ }, [authToken, monitorId]);
+
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 (
{loading ? (
@@ -210,12 +165,12 @@ const DetailsPage = () => {
<>
}
onClick={() => navigate("/monitors")}
sx={{
- backgroundColor: "#f4f4f4",
+ backgroundColor: theme.palette.otherColors.fillGray,
px: theme.gap.ml,
"& svg.MuiSvgIcon-root": {
mr: theme.gap.small,
@@ -224,12 +179,23 @@ const DetailsPage = () => {
}}
/>
-
+
{monitor?.status ? : }
-
- {monitor.url?.replace(/^https?:\/\//, "") || "..."}
-
+
+
+ {monitor.url?.replace(/^https?:\/\//, "") || "..."}
+
+
+
{
Your site is {monitor?.status ? "up" : "down"}.
{" "}
Checking every {formatDurationRounded(monitor?.interval)}.
- Last time checked{" "}
- {formatDurationRounded(getLastChecked(monitor?.checks))} ago.
@@ -294,7 +292,7 @@ const DetailsPage = () => {
justifyContent="space-between"
mb={theme.gap.ml}
>
-
+
Response Times
@@ -330,8 +328,18 @@ const DetailsPage = () => {
/>
-
-
+
+
diff --git a/Client/src/Pages/Monitors/index.jsx b/Client/src/Pages/Monitors/index.jsx
index e56be39b2..11eeb0fb8 100644
--- a/Client/src/Pages/Monitors/index.jsx
+++ b/Client/src/Pages/Monitors/index.jsx
@@ -9,6 +9,7 @@ import { useNavigate } from "react-router-dom";
import Button from "../../Components/Button";
import ServerStatus from "../../Components/Charts/Servers/ServerStatus";
import { useTheme } from "@emotion/react";
+import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded";
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
import OpenInNewPage from "../../assets/icons/open-in-new-page.svg?react";
import Settings from "../../assets/icons/settings-bold.svg?react";
@@ -120,12 +121,11 @@ const Monitors = () => {
};
const closeMenu = () => {
setAnchorEl(null);
- setActions({});
};
const [isOpen, setIsOpen] = useState(false);
const openRemove = () => {
- setAnchorEl(null);
+ closeMenu();
setIsOpen(true);
};
const handleRemove = async (event) => {
@@ -153,17 +153,38 @@ const Monitors = () => {
const down = monitorState.monitors.length - up;
+ // State variable for sorting -> default of 1 for ascending
+ const [sortOrder, setSortOrder] = useState(1);
+ // State variable for updating sorting of default monitors
+ const [sortedMonitors, setSortedMonitors] = useState(monitorState.monitors);
+ // Bool for sorting arrow
+ const [isSorted, setIsSorted] = useState(false);
+
+
+ // Function to handle sorting on click of status text
+ const handleSort = () => {
+ setSortOrder(prevOrder => prevOrder * -1);
+
+ // Sort existing monitors with start of ascending order
+ const monitors = [...monitorState.monitors].sort(a => {
+ return a.status ? -1 * sortOrder : 1 * sortOrder;
+ });
+
+ // Update state of monitors
+ setSortedMonitors(monitors);
+ // Trigger arrow visibility and direction
+ setIsSorted(true);
+ };
+
const data = {
cols: [
{ id: 1, name: "Host" },
{
id: 2,
name: (
-
+
Status
-
-
-
+ {isSorted ? {sortOrder === -1 ? : } : null}
),
},
@@ -174,7 +195,8 @@ const Monitors = () => {
rows: [],
};
- data.rows = monitorState.monitors.map((monitor, idx) => {
+ // Render out sorted monitors/default monitors
+ data.rows = sortedMonitors.map((monitor, idx) => {
const params = {
url: monitor.url,
title: monitor.name,
@@ -220,7 +242,11 @@ const Monitors = () => {
aria-label="monitor actions"
onClick={(event) => {
event.stopPropagation();
- openMenu(event, monitor._id, monitor.url);
+ openMenu(
+ event,
+ monitor._id,
+ monitor.type === "ping" ? null : monitor.url
+ );
}}
sx={{
"&:focus": {
@@ -240,7 +266,7 @@ const Monitors = () => {
let loading = monitorState.isLoading && monitorState.monitors.length === 0;
return (
-
+
{loading ? (
) : (
@@ -250,7 +276,10 @@ const Monitors = () => {
justifyContent="space-between"
alignItems="center"
>
-
+
Hello, {authState.user.firstName}
{monitorState.monitors?.length !== 0 && (
@@ -299,7 +328,8 @@ const Monitors = () => {
{
open={Boolean(anchorEl)}
onClose={closeMenu}
>
-
+ {actions.url !== null ? (
+
+ ) : (
+ ""
+ )}
diff --git a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx
index cb82310c4..e9b271326 100644
--- a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx
+++ b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx
@@ -106,7 +106,7 @@ const CreatePageSpeed = () => {
onClick={() => navigate("/pagespeed")}
sx={{
backgroundColor: theme.palette.otherColors.fillGray,
- mb: theme.gap.large,
+ mb: theme.gap.medium,
px: theme.gap.ml,
"& svg.MuiSvgIcon-root": {
mr: theme.gap.small,
@@ -122,8 +122,6 @@ const CreatePageSpeed = () => {
display: "flex",
flexDirection: "column",
gap: theme.gap.large,
- // TODO
- maxWidth: "1000px",
}}
>
Create a page speed monitor
diff --git a/Client/src/Pages/PageSpeed/Details/index.jsx b/Client/src/Pages/PageSpeed/Details/index.jsx
index 45a031da1..006f2acf3 100644
--- a/Client/src/Pages/PageSpeed/Details/index.jsx
+++ b/Client/src/Pages/PageSpeed/Details/index.jsx
@@ -5,8 +5,10 @@ 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 { getLastChecked } from "../../../Utils/monitorUtils";
+import {
+ formatDuration,
+ formatDurationRounded,
+} from "../../../Utils/timeUtils";
import axiosInstance from "../../../Utils/axiosConfig";
import Button from "../../../Components/Button";
import WestRoundedIcon from "@mui/icons-material/WestRounded";
@@ -185,7 +187,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}`,
@@ -297,7 +299,7 @@ const PageSpeedDetails = () => {
const [expand, setExpand] = useState(false);
let loading = Object.keys(monitor).length === 0;
-
+ const data = monitor?.checks ? [...monitor.checks].reverse() : [];
return (
{loading ? (
@@ -361,7 +363,7 @@ const PageSpeedDetails = () => {
{
title="Last checked"
value={
<>
- {formatDate(getLastChecked(monitor?.checks, false))}{" "}
+ {formatDuration(monitor?.lastChecked)}{" "}
- ({formatDurationRounded(getLastChecked(monitor?.checks))}{" "}
- ago)
+ ago
>
}
@@ -386,17 +387,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,12 +406,10 @@ const PageSpeedDetails = () => {
Score history
-
+
Performance report
-
+
{
let isActuallyLoading = isLoading && monitors.length === 0;
return (
-
+
{isActuallyLoading ? (
) : monitors?.length !== 0 ? (
diff --git a/Client/src/Utils/Theme.js b/Client/src/Utils/Theme.js
index c065798d2..c4faa2d1f 100644
--- a/Client/src/Utils/Theme.js
+++ b/Client/src/Utils/Theme.js
@@ -96,9 +96,10 @@ const theme = createTheme({
ml: "16px",
mlplus: "20px",
large: "24px",
+ lgplus: "32px",
xl: "40px",
xxl: "60px",
- triplexl: "120px"
+ triplexl: "120px",
},
alert: {
info: {
diff --git a/Client/src/assets/icons/down-arrow.svg b/Client/src/assets/icons/down-arrow.svg
new file mode 100644
index 000000000..7c6a715ed
--- /dev/null
+++ b/Client/src/assets/icons/down-arrow.svg
@@ -0,0 +1,3 @@
+
diff --git a/Client/src/index.css b/Client/src/index.css
index 5693ddaa5..58e26ed71 100644
--- a/Client/src/index.css
+++ b/Client/src/index.css
@@ -58,6 +58,7 @@
--env-var-color-30: #fcfcfd;
--env-var-color-31: #1a1919;
--env-var-color-32: #f5f5f5;
+ --env-var-color-33: rgba(83, 83, 83, 0.1);
--env-var-radius-1: 4px;
--env-var-radius-2: 8px;
@@ -155,14 +156,6 @@ button:focus-visible {
border-radius: 4px;
}
-.background-pattern-svg {
- position: absolute;
- top: -5%;
- left: 50%;
- transform: translate(-50%, -50%);
- z-index: -1;
-}
-
.MuiTouchRipple-root {
display: none;
}
diff --git a/README.md b/README.md
index d73109925..2d0db36fc 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,6 @@


-
An open source server monitoring application
@@ -24,31 +23,33 @@ BlueWave Uptime is an open source server monitoring application used to track th
- [x] Incidents at a glance
- [x] Page speed monitoring
- [ ] Scheduled maintenance (in the works)
-- [ ] Status pages (in the works)
**Roadmap (short term):**
+
+- [ ] Memory, disk and CPU monitoring
- [ ] 3rd party integrations
- [ ] DNS monitoring
- [ ] SSL monitoring
**Roadmap (long term):**
-- [ ] Memory, disk and CPU monitoring using OpenTelemetry
+
+- [ ] Status pages
## Tech stack
-* [ReactJs](https://react.dev/)
-* [MUI (React framework)](https://mui.com/)
-* [Node.js](https://nodejs.org/en)
-* [MongoDB](https://mongodb.com)
+- [ReactJs](https://react.dev/)
+- [MUI (React framework)](https://mui.com/)
+- [Node.js](https://nodejs.org/en)
+- [MongoDB](https://mongodb.com)
## Contributing
We love contributors. Here's how you can contribute:
-* Check [Contributor's guideline](https://github.com/bluewave-labs/bluewave-uptime/blob/master/CONTRIBUTING.md).
-* Have a look at our Figma designs [here](https://www.figma.com/design/RPSfaw66HjzSwzntKcgDUV/Uptime-Genie?node-id=0-1&t=WqOFv9jqNTFGItpL-1). We encourage you to copy to your own Figma page, then work on it as it is read-only.
-* Open an issue if you believe you've encountered a bug
-* Make a pull request to add new features/make quality-of-life improvements/fix bugs.
+- Check [Contributor's guideline](https://github.com/bluewave-labs/bluewave-uptime/blob/master/CONTRIBUTING.md).
+- Have a look at our Figma designs [here](https://www.figma.com/design/RPSfaw66HjzSwzntKcgDUV/Uptime-Genie?node-id=0-1&t=WqOFv9jqNTFGItpL-1). We encourage you to copy to your own Figma page, then work on it as it is read-only.
+- Open an issue if you believe you've encountered a bug
+- Make a pull request to add new features/make quality-of-life improvements/fix bugs.
@@ -58,12 +59,12 @@ Made with [contrib.rocks](https://contrib.rocks).

-
## Getting Started
- Clone this repository to your local machine
-1. [Docker Quickstart](#docker-quickstart)
+1. [Quickstart for Developers](#dev-quickstart)
+1. [Docker Compose](#docker-compose)
1. [Installation (Client)](#client)
1. [Configuration(Client)](#config-client)
- [Environment](#env-vars-client)
@@ -117,9 +118,41 @@ Made with [contrib.rocks](https://contrib.rocks).
---
-### Docker Quick Start
+#### Quickstart for Developers
-#### Docker Quickstart
+MAKE SURE YOU CD TO THE SPECIFIED DIRECTORIES AS PATHS IN COMMANDS ARE RELATIVE
+
+##### Cloning and Initial Setup
+
+1. Clone this repository
+2. Checkout the `develop` branch `git checkout develop`
+
+##### Docker Images Setup
+
+3. CD to the `Docker` directory
+4. Run `docker run -d -p 6379:6379 -v $(pwd)/redis/data:/data --name uptime_redis uptime_redis`
+5. Run `docker run -d -p 27017:27017 -v $(pwd)/mongo/data:/data/db --name uptime_database_mongo uptime_database_mongo`
+
+##### Server Setup
+
+6. CD to `Server` directory, run `npm install`
+7. While in `Server` directory, create a `.env` file with the [required environmental variables](#env-vars-server)
+8. While in the `Server` directory, run `npm run dev`
+
+##### Client Setup
+
+9. CD to `Client` directory `run npm install`
+10. While in the `Client` directory, create a `.env` file with the [required environmental variables](#env-vars-client)
+11. While in the `Client` cirectory run `npm run dev`
+
+##### Access Application
+
+12. Client is running at `localhost:5173`
+13. Server is running at `localhost:5000`
+
+---
+
+#### Docker Compose
The fastest way to start the application is to use our Dockerfiles and [Docker Compose](https://docs.docker.com/compose/).
@@ -143,7 +176,7 @@ SYSTEM_EMAIL_ADDRESS=
SYSTEM_EMAIL_PASSWORD=
```
-3. In the `Docker` directory, create a `client.env` file with the [required environtmental variables](#env-vars-client) for the client. Sample file:
+3. In the `Client` directory, create a `client.env` file with the [required environtmental variables](#env-vars-client) for the client. Sample file:
```
VITE_APP_API_BASE_URL="http://localhost:5000/api/v1"
@@ -151,7 +184,7 @@ VITE_APP_API_BASE_URL="http://localhost:5000/api/v1"
4. In the `Docker` directory run `docker compose up` to run the `docker-compose.yaml` file and start all four images.
-That's it, the application is ready to use on port 5173.
+That's it, the application is ready to use on port 80.
### Client
@@ -2112,4 +2145,3 @@ const myRoute = async(req, res, next) => {
```
Errors should not be handled at the controller level and should be left to the middleware to handle.
-
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..28c33caeb 100644
--- a/Server/controllers/monitorController.js
+++ b/Server/controllers/monitorController.js
@@ -7,6 +7,7 @@ const {
getMonitorsByUserIdQueryValidation,
} = require("../validation/joi");
+const sslChecker = require("ssl-checker");
const SERVICE_NAME = "monitorController";
const { errorMessages, successMessages } = require("../utils/messages");
const { runInNewContext } = require("vm");
@@ -33,6 +34,73 @@ 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);
+ }
+};
+
+const getMonitorCertificate = async (req, res, next) => {
+ try {
+ //validation
+ } catch (error) {
+ error.status = 422;
+ error.message =
+ error.details?.[0]?.message || error.message || "Validation Error";
+ next(error);
+ }
+
+ try {
+ const monitor = await req.db.getMonitorById(req, res);
+ const monitorUrl = new URL(monitor.url);
+ const certificate = await sslChecker(monitorUrl.hostname);
+ if (certificate && certificate.validTo) {
+ return res.json({
+ success: true,
+ msg: successMessages.MONITOR_CERTIFICATE,
+ data: {
+ certificateDate: new Date(certificate.validTo).toLocaleDateString(),
+ },
+ });
+ } else {
+ return res.json({
+ success: true,
+ msg: successMessages.MONITOR_CERTIFICATE,
+ data: { certificateDate: "N/A" },
+ });
+ }
+ } catch (error) {
+ error.service = SERVICE_NAME;
+ next(error);
+ }
+};
+
/**
* Returns monitor with matching ID
* @async
@@ -269,6 +337,8 @@ const editMonitor = async (req, res, next) => {
module.exports = {
getAllMonitors,
+ getMonitorStatsById,
+ getMonitorCertificate,
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..0fc56c755 100644
--- a/Server/db/mongo/modules/monitorModule.js
+++ b/Server/db/mongo/modules/monitorModule.js
@@ -22,6 +22,251 @@ 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 = (monitor, checks) => {
+ const checksIn60Mins = Math.floor((60 * 60 * 1000) / monitor.interval);
+ const noBlankChecks = checksIn60Mins - checks.length;
+
+ const statusBarValues = checks.map((check) => {
+ return {
+ status: check.status,
+ responseTime: check.responseTime,
+ value: 75,
+ };
+ });
+
+ for (let i = 0; i < noBlankChecks; i++) {
+ statusBarValues.push({ status: undefined, responseTime: 0, value: 75 });
+ }
+ return statusBarValues.reverse();
+};
+/**
+ * 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, dateRange, 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);
+
+ // Determine if this is a pagespeed monitor or an http/ping monitor
+ let model =
+ monitor.type === "http" || monitor.type === "ping"
+ ? Check
+ : PageSpeedCheck;
+
+ // Build monitor stats object
+ const monitorStats = {
+ ...monitor.toObject(),
+ };
+
+ // Start building query
+ const checksQuery = { monitorId: monitor._id };
+
+ // Get all checks
+ const checksAll = await model.find(checksQuery).sort({
+ createdAt: sortOrder,
+ });
+
+ if (monitor.type === "http" || monitor.type === "ping") {
+ const checksQuery24Hours = {
+ ...checksQuery,
+ createdAt: { $gte: filterLookup.day },
+ };
+ const checksQuery30Days = {
+ ...checksQuery,
+ createdAt: { $gte: filterLookup.month },
+ };
+ const checksQuery60Mins = {
+ ...checksQuery,
+ 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 }),
+ ]);
+
+ // HTTP/PING Specific stats
+ monitorStats.avgResponseTime24hours =
+ getAverageResponseTime24Hours(checks24Hours);
+ monitorStats.uptime24Hours = getUptimePercentage(checks24Hours);
+ monitorStats.uptime30Days = getUptimePercentage(checks30Days);
+ monitorStats.statusBar = getStatusBarValues(monitor, checks60Mins);
+ }
+
+ //Get checks for dateRange
+ if (status !== undefined) {
+ checksQuery.status = status;
+ }
+
+ // Filter checks by "day", "week", or "month"
+ if (dateRange !== undefined) {
+ checksQuery.createdAt = { $gte: filterLookup[dateRange] };
+ }
+
+ let dateRangeChecks = await model
+ .find(checksQuery)
+ .sort({
+ createdAt: sortOrder,
+ })
+ .limit(limit);
+
+ const incidents = getIncidents(dateRangeChecks);
+
+ // 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 and stats that depend on the dateRange
+ monitorStats.uptimeDuration = calculateUptimeDuration(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
@@ -30,69 +275,10 @@ const getAllMonitors = async (req, res) => {
* @returns {Promise}
* @throws {Error}
*/
-const getMonitorById = async (req, res) => {
+const getMonitorById = async (monitorId) => {
try {
- const { monitorId } = req.params;
- let { status, limit, sortOrder, filter, numToDisplay, normalize } =
- req.query;
-
- const filterLookup = {
- 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)),
- };
-
- // This effectively removes limit, returning all checks
- if (limit === undefined) limit = 0;
-
- // Default sort order is newest -> oldest
- if (sortOrder === "asc") {
- sortOrder = 1;
- } else if (sortOrder === "desc") {
- sortOrder = -1;
- } else sortOrder = -1;
-
const monitor = await Monitor.findById(monitorId);
-
- const checksQuery = { monitorId: monitor._id };
-
- if (status !== undefined) {
- checksQuery.status = status;
- }
-
- // Filter checks by "day", "week", or "month"
- if (filter !== undefined) {
- checksQuery.createdAt = { $gte: filterLookup[filter] };
- }
-
- // Determine model type
- let model =
- monitor.type === "http" || monitor.type === "ping"
- ? Check
- : PageSpeedCheck;
-
- let checks = await model
- .find(checksQuery)
- .sort({
- createdAt: sortOrder,
- })
- .limit(limit);
-
- // If more than numToDisplay checks, pick every nth check
- if (numToDisplay !== undefined && checks && checks.length > numToDisplay) {
- const n = Math.ceil(checks.length / numToDisplay);
- checks = checks.filter(
- (_, index) => index % n === 0 || index === checks.length - 1
- );
- }
-
- // Normalize checks if requested
- if (normalize !== undefined) {
- checks = NormalizeData(checks, 1, 100);
- }
- const notifications = await Notification.find({ monitorId: monitor._id });
- const monitorWithChecks = { ...monitor.toObject(), checks, notifications };
- return monitorWithChecks;
+ return monitor;
} catch (error) {
throw error;
}
@@ -263,6 +449,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/index.js b/Server/index.js
index 1eb5d886b..110057987 100644
--- a/Server/index.js
+++ b/Server/index.js
@@ -16,6 +16,7 @@ const maintenanceWindowRouter = require("./routes/maintenanceWindowRoute");
const { connectDbAndRunServer } = require("./configs/db");
const queueRouter = require("./routes/queueRoute");
const JobQueue = require("./service/jobQueue");
+const NetworkService = require("./service/networkService");
const EmailService = require("./service/emailService");
const PageSpeedService = require("./service/pageSpeedService");
@@ -114,8 +115,9 @@ const startApp = async () => {
// Create services
await connectDbAndRunServer(app, db);
- const jobQueue = await JobQueue.createJobQueue(db);
const emailService = new EmailService();
+ const networkService = new NetworkService(db, emailService);
+ const jobQueue = await JobQueue.createJobQueue(db, networkService);
const pageSpeedService = new PageSpeedService();
const cleanup = async () => {
diff --git a/Server/models/Check.js b/Server/models/Check.js
index aeb4f4849..55bb2d988 100644
--- a/Server/models/Check.js
+++ b/Server/models/Check.js
@@ -57,6 +57,7 @@ const CheckSchema = mongoose.Schema(
*
* @type {Date}
*/
+
expiry: {
type: Date,
default: Date.now,
@@ -68,66 +69,4 @@ const CheckSchema = mongoose.Schema(
}
);
-/**
- * Pre-save middleware to handle status change notifications.
- *
- * This middleware checks if the status of the monitor associated
- * with the check has changed and sends notifications if necessary.
- */
-CheckSchema.pre("save", async function (next) {
- try {
- const monitor = await mongoose.model("Monitor").findById(this.monitorId);
-
- if (monitor) {
- const notifications = await Notification.find({
- monitorId: this.monitorId,
- });
-
- // Check if there are any notifications
- if (notifications && notifications.length > 0) {
- // Only send email if monitor status has changed
- if (monitor.status !== this.status) {
- const emailService = new EmailService();
-
- if (monitor.status === true && this.status === false) {
- // Notify users that the monitor is down
- for (const notification of notifications) {
- if (notification.type === "email") {
- await emailService.buildAndSendEmail(
- "serverIsDownTemplate",
- { monitorName: monitor.name, monitorUrl: monitor.url },
- notification.address,
- `Monitor ${monitor.name} is down`
- );
- }
- }
- }
-
- if (monitor.status === false && this.status === true) {
- // Notify users that the monitor is up
- for (const notification of notifications) {
- if (notification.type === "email") {
- await emailService.buildAndSendEmail(
- "serverIsUpTemplate",
- { monitorName: monitor.name, monitorUrl: monitor.url },
- notification.address,
- `Monitor ${monitor.name} is back up`
- );
- }
- }
- }
-
- // Update monitor status
- monitor.status = this.status;
- await monitor.save();
- }
- }
- }
- } catch (error) {
- console.log(error);
- } finally {
- next();
- }
-});
-
module.exports = mongoose.model("Check", CheckSchema);
diff --git a/Server/package-lock.json b/Server/package-lock.json
index 916c66648..88acafb10 100644
--- a/Server/package-lock.json
+++ b/Server/package-lock.json
@@ -9,8 +9,8 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
- "@sendgrid/mail": "^8.1.3",
- "axios": "^1.7.2",
+ "@sendgrid/mail": "^7.7.0",
+ "axios": "^1.3.1",
"bcrypt": "^5.1.1",
"bullmq": "5.7.15",
"cors": "^2.8.5",
@@ -27,6 +27,7 @@
"nodemailer": "^6.9.14",
"ping": "0.4.4",
"sharp": "0.33.4",
+ "ssl-checker": "2.0.10",
"winston": "^3.13.0"
},
"devDependencies": {
@@ -772,38 +773,50 @@
}
},
"node_modules/@sendgrid/client": {
- "version": "8.1.3",
- "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-8.1.3.tgz",
- "integrity": "sha512-mRwTticRZIdUTsnyzvlK6dMu3jni9ci9J+dW/6fMMFpGRAJdCJlivFVYQvqk8kRS3RnFzS7sf6BSmhLl1ldDhA==",
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-7.7.0.tgz",
+ "integrity": "sha512-SxH+y8jeAQSnDavrTD0uGDXYIIkFylCo+eDofVmZLQ0f862nnqbC3Vd1ej6b7Le7lboyzQF6F7Fodv02rYspuA==",
+ "license": "MIT",
"dependencies": {
- "@sendgrid/helpers": "^8.0.0",
- "axios": "^1.6.8"
+ "@sendgrid/helpers": "^7.7.0",
+ "axios": "^0.26.0"
},
"engines": {
- "node": ">=12.*"
+ "node": "6.* || 8.* || >=10.*"
+ }
+ },
+ "node_modules/@sendgrid/client/node_modules/axios": {
+ "version": "0.26.1",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
+ "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.14.8"
}
},
"node_modules/@sendgrid/helpers": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-8.0.0.tgz",
- "integrity": "sha512-Ze7WuW2Xzy5GT5WRx+yEv89fsg/pgy3T1E3FS0QEx0/VvRmigMZ5qyVGhJz4SxomegDkzXv/i0aFPpHKN8qdAA==",
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-7.7.0.tgz",
+ "integrity": "sha512-3AsAxfN3GDBcXoZ/y1mzAAbKzTtUZ5+ZrHOmWQ279AuaFXUNCh9bPnRpN504bgveTqoW+11IzPg3I0WVgDINpw==",
+ "license": "MIT",
"dependencies": {
"deepmerge": "^4.2.2"
},
"engines": {
- "node": ">= 12.0.0"
+ "node": ">= 6.0.0"
}
},
"node_modules/@sendgrid/mail": {
- "version": "8.1.3",
- "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-8.1.3.tgz",
- "integrity": "sha512-Wg5iKSUOER83/cfY6rbPa+o3ChnYzWwv1OcsR8gCV8SKi+sUPIMroildimlnb72DBkQxcbylxng1W7f0RIX7MQ==",
+ "version": "7.7.0",
+ "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-7.7.0.tgz",
+ "integrity": "sha512-5+nApPE9wINBvHSUxwOxkkQqM/IAAaBYoP9hw7WwgDNQPxraruVqHizeTitVtKGiqWCKm2mnjh4XGN3fvFLqaw==",
+ "license": "MIT",
"dependencies": {
- "@sendgrid/client": "^8.1.3",
- "@sendgrid/helpers": "^8.0.0"
+ "@sendgrid/client": "^7.7.0",
+ "@sendgrid/helpers": "^7.7.0"
},
"engines": {
- "node": ">=12.*"
+ "node": "6.* || 8.* || >=10.*"
}
},
"node_modules/@sideway/address": {
@@ -980,14 +993,15 @@
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
- "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
},
"node_modules/axios": {
- "version": "1.7.2",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
- "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
+ "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
"dependencies": {
- "follow-redirects": "^1.15.6",
+ "follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
@@ -1366,6 +1380,7 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
@@ -1710,6 +1725,7 @@
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+ "license": "MIT",
"engines": {
"node": ">=0.10.0"
}
@@ -1734,6 +1750,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
"engines": {
"node": ">=0.4.0"
}
@@ -2069,6 +2086,7 @@
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
+ "license": "MIT",
"engines": {
"node": ">=4.0"
},
@@ -2108,6 +2126,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
@@ -4615,7 +4634,8 @@
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
- "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
},
"node_modules/pstree.remy": {
"version": "1.1.8",
@@ -5026,6 +5046,12 @@
"memory-pager": "^1.0.2"
}
},
+ "node_modules/ssl-checker": {
+ "version": "2.0.10",
+ "resolved": "https://registry.npmjs.org/ssl-checker/-/ssl-checker-2.0.10.tgz",
+ "integrity": "sha512-SS6rrZocToJWHM1p6iVNb583ybB3UqT1fymCHSWuEdXDUqKA6O1D5Fb8KJVmhj3XKXE82IEWcr+idJrc4jUzFQ==",
+ "license": "MIT"
+ },
"node_modules/stack-trace": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
diff --git a/Server/package.json b/Server/package.json
index ad0936654..eb31dd480 100644
--- a/Server/package.json
+++ b/Server/package.json
@@ -11,7 +11,6 @@
"author": "",
"license": "ISC",
"dependencies": {
- "@sendgrid/mail": "^8.1.3",
"axios": "^1.7.2",
"bcrypt": "^5.1.1",
"bullmq": "5.7.15",
@@ -29,6 +28,7 @@
"nodemailer": "^6.9.14",
"ping": "0.4.4",
"sharp": "0.33.4",
+ "ssl-checker": "2.0.10",
"winston": "^3.13.0"
},
"devDependencies": {
diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js
index 2622181a3..847b2b25d 100644
--- a/Server/routes/monitorRoute.js
+++ b/Server/routes/monitorRoute.js
@@ -4,6 +4,8 @@ const { verifyOwnership } = require("../middleware/verifyOwnership");
const Monitor = require("../models/Monitor");
router.get("/", monitorController.getAllMonitors);
+router.get("/stats/:monitorId", monitorController.getMonitorStatsById);
+router.get("/certificate/:monitorId", monitorController.getMonitorCertificate);
router.get("/:monitorId", monitorController.getMonitorById);
router.get("/user/:userId", monitorController.getMonitorsByUserId);
diff --git a/Server/service/jobQueue.js b/Server/service/jobQueue.js
index 12c151abd..4c7a02192 100644
--- a/Server/service/jobQueue.js
+++ b/Server/service/jobQueue.js
@@ -16,7 +16,7 @@ class JobQueue {
* @constructor
* @throws {Error}
*/
- constructor() {
+ constructor(networkService) {
this.queue = new Queue(QUEUE_NAME, {
connection,
});
@@ -32,11 +32,11 @@ class JobQueue {
* @returns {Promise} - Returns a new JobQueue
*
*/
- static async createJobQueue(db) {
+ static async createJobQueue(db, networkService) {
const queue = new JobQueue();
try {
queue.db = db;
- queue.networkService = new NetworkService(db);
+ queue.networkService = networkService;
const monitors = await db.getAllMonitors();
for (const monitor of monitors) {
await queue.addJob(monitor.id, monitor);
diff --git a/Server/service/networkService.js b/Server/service/networkService.js
index dc8994945..375380f2e 100644
--- a/Server/service/networkService.js
+++ b/Server/service/networkService.js
@@ -3,8 +3,9 @@ const ping = require("ping");
const logger = require("../utils/logger");
class NetworkService {
- constructor(db) {
+ constructor(db, emailService) {
this.db = db;
+ this.emailService = emailService;
this.TYPE_PING = "ping";
this.TYPE_HTTP = "http";
this.TYPE_PAGESPEED = "pagespeed";
@@ -12,6 +13,33 @@ class NetworkService {
this.NETWORK_ERROR = 5000;
}
+ async handleNotification(job, isAlive) {
+ const { _id } = job.data;
+ const monitor = await this.db.getMonitorById(_id);
+
+ // If monitor status changes, update monitor status and send notification
+ if (monitor.status !== isAlive) {
+ monitor.status = !monitor.status;
+ await monitor.save();
+
+ let template =
+ isAlive === true ? "serverIsUpTemplate" : "serverIsDownTemplate";
+ let status = isAlive === true ? "up" : "down";
+
+ const notifications = await this.db.getNotificationsByMonitorId(_id);
+ for (const notification of notifications) {
+ if (notification.type === "email") {
+ await this.emailService.buildAndSendEmail(
+ template,
+ { monitorName: monitor.name, monitorUrl: monitor.url },
+ notification.address,
+ `Monitor ${monitor.name} is ${status}`
+ );
+ }
+ }
+ }
+ }
+
/**
* Measures the response time of an asynchronous operation.
* @param {Function} operation - An asynchronous operation to measure.
@@ -42,10 +70,12 @@ class NetworkService {
return response;
};
+ let isAlive;
+
try {
const { responseTime, response } =
await this.measureResponseTime(operation);
- const isAlive = response.alive;
+ isAlive = response.alive;
const checkData = {
monitorId: job.data._id,
@@ -54,12 +84,15 @@ class NetworkService {
};
return await this.logAndStoreCheck(checkData, this.db.createCheck);
} catch (error) {
+ isAlive = false;
const checkData = {
monitorId: job.data._id,
- status: false,
+ status: isAlive,
responseTime: error.responseTime,
};
return await this.logAndStoreCheck(checkData, this.db.createCheck);
+ } finally {
+ this.handleNotification(job, isAlive);
}
}
@@ -75,13 +108,15 @@ class NetworkService {
return response;
};
+ let isAlive;
+
// attempt connection
try {
const { responseTime, response } =
await this.measureResponseTime(operation);
// check if response is in the 200 range, if so, service is up
- const isAlive = response.status >= 200 && response.status < 300;
+ isAlive = response.status >= 200 && response.status < 300;
//Create a check with relevant data
const checkData = {
@@ -92,9 +127,10 @@ class NetworkService {
};
return await this.logAndStoreCheck(checkData, this.db.createCheck);
} catch (error) {
+ isAlive = false;
const checkData = {
monitorId: job.data._id,
- status: false,
+ status: isAlive,
responseTime: error.responseTime,
};
// The server returned a response
@@ -104,6 +140,8 @@ class NetworkService {
checkData.statusCode = this.NETWORK_ERROR;
}
return await this.logAndStoreCheck(checkData, this.db.createCheck);
+ } finally {
+ this.handleNotification(job, isAlive);
}
}
@@ -122,6 +160,8 @@ class NetworkService {
* @throws {Error} Throws an error if there is an issue with fetching or processing the PageSpeed insights.
*/
async handlePagespeed(job) {
+ let isAlive;
+
try {
const url = job.data.url;
const response = await axios.get(
@@ -145,9 +185,10 @@ class NetworkService {
// Total Blocking Time 30%
// Cumulative Layout Shift 25%
+ isAlive = true;
const checkData = {
monitorId: job.data._id,
- status: true,
+ status: isAlive,
accessibility: (categories.accessibility?.score || 0) * 100,
bestPractices: (categories["best-practices"]?.score || 0) * 100,
seo: (categories.seo?.score || 0) * 100,
@@ -163,15 +204,18 @@ class NetworkService {
this.logAndStoreCheck(checkData, this.db.createPageSpeedCheck);
} catch (error) {
+ isAlive = false;
const checkData = {
monitorId: job.data._id,
- status: false,
+ status: isAlive,
accessibility: 0,
bestPractices: 0,
seo: 0,
performance: 0,
};
this.logAndStoreCheck(checkData, this.db.createPageSpeedCheck);
+ } finally {
+ this.handleNotification(job, isAlive);
}
}
diff --git a/Server/utils/messages.js b/Server/utils/messages.js
index 40d7205e5..92f568605 100644
--- a/Server/utils/messages.js
+++ b/Server/utils/messages.js
@@ -71,11 +71,13 @@ 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",
MONITOR_DELETE: "Monitor deleted successfully",
MONITOR_EDIT: "Monitor edited successfully",
+ MONITOR_CERTIFICATE: "Got monitor certificate successfully",
//Job Queue
JOB_QUEUE_DELETE_JOB: "Job removed successfully",
diff --git a/Server/validation/joi.js b/Server/validation/joi.js
index 74bf5ed92..fe7090083 100644
--- a/Server/validation/joi.js
+++ b/Server/validation/joi.js
@@ -126,7 +126,7 @@ const getMonitorByIdQueryValidation = joi.object({
status: joi.boolean(),
sortOrder: joi.string().valid("asc", "desc"),
limit: joi.number(),
- filter: joi.string().valid("day", "week", "month"),
+ dateRange: joi.string().valid("day", "week", "month"),
numToDisplay: joi.number(),
normalize: joi.boolean(),
});