diff --git a/Client/src/App.css b/Client/src/App.css index 0d669ffa5..bf3d4ea92 100644 --- a/Client/src/App.css +++ b/Client/src/App.css @@ -1,31 +1,3 @@ -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - .card { padding: 2em; } diff --git a/Client/src/App.jsx b/Client/src/App.jsx index 12db9dab9..783f28330 100644 --- a/Client/src/App.jsx +++ b/Client/src/App.jsx @@ -7,7 +7,7 @@ import Login from "./Pages/Auth/Login"; import Register from "./Pages/Auth/Register/Register"; import HomeLayout from "./Layouts/HomeLayout"; import Account from "./Pages/Account"; -import Monitors from "./Pages/Monitors"; +import Monitors from "./Pages/Monitors/Home"; import CreateMonitor from "./Pages/Monitors/CreateMonitor"; import Incidents from "./Pages/Incidents"; import Status from "./Pages/Status"; @@ -21,27 +21,40 @@ import ProtectedRoute from "./Components/ProtectedRoute"; import Details from "./Pages/Monitors/Details"; import Maintenance from "./Pages/Maintenance"; import withAdminCheck from "./HOC/withAdminCheck"; +import withAdminProp from "./HOC/withAdminProp"; import Configure from "./Pages/Monitors/Configure"; import PageSpeed from "./Pages/PageSpeed"; import CreatePageSpeed from "./Pages/PageSpeed/CreatePageSpeed"; import CreateNewMaintenanceWindow from "./Pages/Maintenance/CreateMaintenanceWindow"; import PageSpeedDetails from "./Pages/PageSpeed/Details"; import PageSpeedConfigure from "./Pages/PageSpeed/Configure"; +import { ThemeProvider } from "@emotion/react"; +import lightTheme from "./Utils/Theme/lightTheme"; +import darkTheme from "./Utils/Theme/darkTheme"; +import { useSelector } from "react-redux"; function App() { const AdminCheckedRegister = withAdminCheck(Register); + const MonitorsWithAdminProp = withAdminProp(Monitors); + const DetailsWithAdminProp = withAdminProp(Details); + const PageSpeedWithAdminProp = withAdminProp(PageSpeed); + const MaintenanceWithAdminProp = withAdminProp(Maintenance); + const SettingsWithAdminProp = withAdminProp(Settings); + + const mode = useSelector((state) => state.ui.mode); + return ( - <> + }> } + element={} /> } + element={} /> } + element={} /> } + element={} /> } + element={} /> } + element={} /> - + ); } diff --git a/Client/src/Components/Alert/index.jsx b/Client/src/Components/Alert/index.jsx index 61c3b6bc3..eb5fbd13b 100644 --- a/Client/src/Components/Alert/index.jsx +++ b/Client/src/Components/Alert/index.jsx @@ -1,12 +1,11 @@ import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; -import { Box, IconButton, Stack, Typography } from "@mui/material"; +import { Box, Button, IconButton, Stack, Typography } from "@mui/material"; import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined"; import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined"; import CloseIcon from "@mui/icons-material/Close"; import "./index.css"; -import Button from "../Button"; /** * Icons mapping for different alert variants. @@ -32,7 +31,7 @@ const icons = { const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => { const theme = useTheme(); - const { bg, border, color } = theme.alert[variant]; + const { text, light, border } = theme.palette[variant]; const icon = icons[variant]; return ( @@ -41,46 +40,43 @@ const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => { justifyContent="flex-start" alignItems={hasIcon ? "" : "center"} className="alert row-stack" - gap={theme.gap.ml} + gap={theme.spacing(8)} sx={{ - padding: hasIcon ? theme.gap.ml : `${theme.gap.small} ${theme.gap.ml}`, - backgroundColor: bg, + padding: hasIcon + ? theme.spacing(8) + : `${theme.spacing(4)} ${theme.spacing(8)}`, + backgroundColor: light, border: `solid 1px ${border}`, - borderRadius: `${theme.shape.borderRadius}px`, + borderRadius: theme.shape.borderRadius, }} > - {hasIcon && {icon}} + {hasIcon && {icon}} {title && ( - + {title} )} {body && ( - + {body} )} {hasIcon && isToast && ( + > + Dismiss + )} {isToast && ( @@ -95,10 +91,6 @@ const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => { "&:focus": { outline: "none", }, - "& .MuiTouchRipple-root": { - pointerEvents: "none", - display: "none", - }, }} > { } sx={{ fontSize: small ? "16px" : "22px", + color: "white", fontWeight: 400, backgroundColor: stringToColor(`${user?.firstName} ${user?.lastName}`), display: "inline-flex", diff --git a/Client/src/Components/BasicTable/index.css b/Client/src/Components/BasicTable/index.css index 208604021..5defa124d 100644 --- a/Client/src/Components/BasicTable/index.css +++ b/Client/src/Components/BasicTable/index.css @@ -17,8 +17,6 @@ .MuiPaper-root:has(table.MuiTable-root) { box-shadow: none; - border: solid 1px var(--env-var-color-16); - border-radius: var(--env-var-radius-1); } .MuiTable-root .MuiTableBody-root @@ -26,10 +24,7 @@ .MuiTableCell-root { border: none; } -.MuiTable-root .MuiTableHead-root, -.MuiTable-root .MuiTableRow-root:hover { - background-color: var(--env-var-color-13); -} + .MuiTable-root .MuiTableHead-root .MuiTableCell-root, .MuiTable-root .MuiTableBody-root .MuiTableCell-root { font-size: var(--env-var-font-size-medium); @@ -37,7 +32,6 @@ .MuiTable-root .MuiTableHead-root .MuiTableCell-root { padding: var(--env-var-spacing-1) var(--env-var-spacing-2); font-weight: 500; - color: var(--env-var-color-2); } .MuiTable-root .MuiTableHead-root span { display: inline-block; @@ -52,7 +46,6 @@ height: 20px; } .MuiTable-root .MuiTableBody-root .MuiTableCell-root { - color: var(--env-var-color-5); padding: 6px var(--env-var-spacing-2); } .MuiTable-root .MuiTableBody-root .MuiTableRow-root { @@ -60,21 +53,14 @@ } .MuiPaper-root + .MuiPagination-root { - 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); } .MuiPaper-root + .MuiPagination-root ul { justify-content: center; } -.MuiPaper-root - + .MuiPagination-root - ul - li:not(:first-child):not(:last-child) - button { +.MuiPaper-root + .MuiPagination-root button { font-size: var(--env-var-font-size-medium); - color: var(--env-var-color-5); font-weight: 500; } .MuiPaper-root + .MuiPagination-root ul li:first-child { @@ -83,11 +69,6 @@ .MuiPaper-root + .MuiPagination-root ul li:last-child { margin-left: auto; } -.MuiPaper-root + .MuiPagination-root ul li:first-child button, -.MuiPaper-root + .MuiPagination-root ul li:last-child button { - border: solid 1px var(--env-var-color-16); - border-radius: var(--env-var-radius-1); -} .MuiPaper-root + .MuiPagination-root ul li:first-child button { padding: 0 var(--env-var-spacing-1) 0 var(--env-var-spacing-1-plus); } @@ -107,23 +88,14 @@ content: "Next"; margin-right: 15px; } -.MuiPaper-root + .MuiPagination-root .MuiPaginationItem-root.Mui-selected { - background-color: var(--env-var-color-15); -} .MuiPaper-root + .MuiPagination-root div.MuiPaginationItem-root { user-select: none; } .MuiTablePagination-root p { - color: var(--env-var-color-2); font-weight: 500; font-size: var(--env-var-font-size-small-plus); } -.MuiTablePagination-root button, -.MuiTablePagination-root .MuiSelect-select { - border: solid 1px #ebebeb; - border-radius: var(--env-var-radius-1); -} .MuiTablePagination-root .MuiTablePagination-select.MuiSelect-select { text-align: left; text-align-last: left; @@ -137,10 +109,6 @@ width: 22px; height: 22px; } -.MuiTablePagination-root svg path { - stroke: var(--env-var-color-2); - stroke-width: 1.3; -} .MuiTablePagination-root .MuiSelect-icon { width: 16px; height: 16px; @@ -151,38 +119,9 @@ .MuiTablePagination-root button.Mui-disabled { opacity: 0.4; } -.MuiTablePagination-root button:not(.Mui-disabled):hover { - background-color: #f4f4f4; - border-color: #f4f4f4; -} - -.pagination-dropdown.MuiPaper-root { - box-shadow: var(--env-var-shadow-1); - border: solid 1px var(--env-var-color-6); - border-radius: var(--env-var-radius-1); -} -.pagination-dropdown ul { - padding: 4px; - min-width: 100px; -} -.pagination-dropdown li, -body:has(.pagination-dropdown) p:has(+ .MuiTablePagination-root) { - font-size: var(--env-var-font-size-medium); - color: var(--env-var-color-2); - padding: 4px; -} -.pagination-dropdown li.Mui-selected, -.pagination-dropdown li.Mui-selected.Mui-focusVisible, -.pagination-dropdown li.Mui-selected:hover { - background-color: #f4f4f4; -} -body:has(.pagination-dropdown) p:has(+ .MuiTablePagination-root) { - font-size: var(--env-var-font-size-small-plus); -} .monitors .MuiTable-root .MuiTableHead-root .MuiTableCell-root { text-transform: uppercase; - color: var(--env-var-color-5); opacity: 0.8; font-size: var(--env-var-font-size-small-plus); font-weight: 400; diff --git a/Client/src/Components/BasicTable/index.jsx b/Client/src/Components/BasicTable/index.jsx index c723d2bec..e9fbbadfd 100644 --- a/Client/src/Components/BasicTable/index.jsx +++ b/Client/src/Components/BasicTable/index.jsx @@ -13,6 +13,7 @@ import { Box, Typography, Stack, + Button, } from "@mui/material"; import { useDispatch, useSelector } from "react-redux"; import { setRowsPerPage } from "../../Features/UI/uiSlice"; @@ -21,7 +22,6 @@ import RightArrowDouble from "../../assets/icons/right-arrow-double.svg?react"; import LeftArrow from "../../assets/icons/left-arrow.svg?react"; import RightArrow from "../../assets/icons/right-arrow.svg?react"; import SelectorVertical from "../../assets/icons/selector-vertical.svg?react"; -import Button from "../Button"; import "./index.css"; /** * Component for pagination actions (first, previous, next, last). @@ -54,37 +54,37 @@ const TablePaginationActions = (props) => { return ( ); }; @@ -215,6 +215,9 @@ const BasicTable = ({ data, paginated, reversed, table }) => { { direction="row" alignItems="center" justifyContent="space-between" - px={theme.gap.small} + px={theme.spacing(4)} + sx={{ + "& p": { + color: theme.palette.text.tertiary, + }, + }} > - + Showing {getRange()} of {data.rows.length} monitor(s) { keepMounted: true, PaperProps: { className: "pagination-dropdown", + sx: { + mt: 0, + mb: theme.spacing(2), + }, }, transformOrigin: { vertical: "bottom", horizontal: "left" }, anchorOrigin: { vertical: "top", horizontal: "left" }, - sx: { mt: "-4px" }, + sx: { mt: theme.spacing(-2) }, }, inputProps: { id: "pagination-dropdown" }, IconComponent: SelectorVertical, sx: { - ml: theme.gap.small, - mr: theme.gap.large, - minWidth: theme.gap.xl, + ml: theme.spacing(4), + mr: theme.spacing(12), + minWidth: theme.spacing(20), textAlign: "left", "&.Mui-focused > div": { - backgroundColor: theme.palette.otherColors.white, + backgroundColor: theme.palette.background.main, }, }, }, }} sx={{ - mt: theme.gap.medium, - color: theme.palette.otherColors.bluishGray, + mt: theme.spacing(6), + color: theme.palette.text.secondary, + "& svg path": { + stroke: theme.palette.text.tertiary, + strokeWidth: 1.3, + }, + "& .MuiSelect-select": { + border: 1, + borderColor: theme.palette.border.light, + borderRadius: theme.shape.borderRadius, + }, }} /> diff --git a/Client/src/Components/Breadcrumbs/index.css b/Client/src/Components/Breadcrumbs/index.css index b7d190de1..a425946e8 100644 --- a/Client/src/Components/Breadcrumbs/index.css +++ b/Client/src/Components/Breadcrumbs/index.css @@ -1,5 +1,5 @@ .MuiBreadcrumbs-root { - height: 36px; + height: 34px; } .MuiBreadcrumbs-root svg { width: 16px; @@ -7,17 +7,8 @@ } .MuiBreadcrumbs-root .MuiBreadcrumbs-li a { font-size: var(--env-var-font-size-medium); - color: var(--env-var-color-2); - opacity: 0.8; font-weight: 400; } -.MuiBreadcrumbs-root .MuiBreadcrumbs-li:hover a { - color: var(--env-var-color-2); -} -.MuiBreadcrumbs-root .MuiBreadcrumbs-li:not(:last-child):hover a { - opacity: 1; - background-color: #e3e3e3; -} .MuiBreadcrumbs-root .MuiBreadcrumbs-li:not(:last-child) { cursor: pointer; } diff --git a/Client/src/Components/Breadcrumbs/index.jsx b/Client/src/Components/Breadcrumbs/index.jsx index 506263fce..f2ca2089f 100644 --- a/Client/src/Components/Breadcrumbs/index.jsx +++ b/Client/src/Components/Breadcrumbs/index.jsx @@ -25,23 +25,36 @@ const Breadcrumbs = ({ list }) => { } aria-label="breadcrumb" - p={theme.gap.small} + px={theme.spacing(2)} + py={theme.spacing(3.5)} width="fit-content" - backgroundColor={theme.palette.otherColors.fillGray} - borderRadius={`${theme.shape.borderRadius}px`} + backgroundColor={theme.palette.background.fill} + borderRadius={theme.shape.borderRadius} lineHeight="18px" + sx={{ + "& .MuiBreadcrumbs-li:not(:last-of-type):hover a": { + backgroundColor: theme.palette.other.fill, + opacity: 1, + }, + }} > {list.map((item, index) => { return ( navigate(item.path)} + sx={{ + opacity: 0.8, + textTransform: "capitalize", + "&, &:hover": { + color: theme.palette.text.tertiary, + }, + }} > {item.name} diff --git a/Client/src/Components/Button/index.css b/Client/src/Components/Button/index.css deleted file mode 100644 index 63791d46b..000000000 --- a/Client/src/Components/Button/index.css +++ /dev/null @@ -1,33 +0,0 @@ -.MuiButtonBase-root.button-rotate90 svg { - transition: transform 500ms ease; -} -.MuiButtonBase-root.button-rotate90:hover svg { - transform: rotate(90deg); - transform-origin: center; -} - -.MuiButtonBase-root.button-rotate180 svg { - transition: transform 700ms ease; -} -.MuiButtonBase-root.button-rotate180:hover svg { - transform: rotate(180deg); - transform-origin: center; -} - -.MuiButtonBase-root.button-slideLeft { - padding-left: 45px; -} -.MuiButtonBase-root.button-slideLeft svg { - position: absolute; - left: 15px; - transition: left 250ms cubic-bezier(0.65, 0, 0.076, 1); -} -.MuiButtonBase-root.button-slideLeft:hover svg { - left: 11px; -} - -.MuiButtonBase-root.button-slideLeft:hover, -.MuiButtonBase-root.button-rotate180:hover, -.MuiButtonBase-root.button-rotate90:hover { - background-color: rgba(71, 84, 103, 0.08); -} diff --git a/Client/src/Components/Button/index.jsx b/Client/src/Components/Button/index.jsx deleted file mode 100644 index 485973498..000000000 --- a/Client/src/Components/Button/index.jsx +++ /dev/null @@ -1,107 +0,0 @@ -import { Button as MuiButton } from "@mui/material"; -import PropTypes from "prop-types"; -import "./index.css"; - -const levelConfig = { - primary: { - variant: "contained", - color: "primary", - }, - secondary: { - variant: "outlined", - color: "secondary", - }, - tertiary: { - variant: "text", - color: "tertiary", - }, - error: { - variant: "contained", - color: "error", - }, - imagePrimary: { - color: "primary", - variant: "text", - }, - imageSecondary: { - color: "secondary", - variant: "text", - }, - imageTertiary: { - color: "tertiary", - variant: "text", - }, -}; - -/** - * @component - * @param {Object} props - * @param {'primary' | 'secondary' | 'tertiary' | 'error' | 'imagePrimary' | 'imageSecondary' | 'imageTertiary'} props.level - The level of the button - * @param {string} props.type - The type of the button - * @param {string} props.label - The label of the button - * @param {React.ReactNode} props.img - Image for button - * @param {boolean} [props.disabled] - Whether the button is disabled - * @param {function} props.onClick - Function to run when the button is clicked - * @param {Object} props.sx - Styles for the button - * @returns {JSX.Element} - * @example - * // Render an error button - * + )} ); }; @@ -60,6 +81,7 @@ Fallback.propTypes = { title: PropTypes.string.isRequired, checks: PropTypes.arrayOf(PropTypes.string).isRequired, link: PropTypes.string, + isAdmin: PropTypes.bool, }; export default Fallback; diff --git a/Client/src/Components/Inputs/Checkbox/index.css b/Client/src/Components/Inputs/Checkbox/index.css index 0d2285955..e69de29bb 100644 --- a/Client/src/Components/Inputs/Checkbox/index.css +++ b/Client/src/Components/Inputs/Checkbox/index.css @@ -1,14 +0,0 @@ -.checkbox-wrapper { - border-radius: var(--env-var-radius-1); -} -.checkbox-wrapper:not(:has(.Mui-disabled)):hover { - background-color: var(--env-var-color-15); -} -.checkbox-wrapper span.MuiTypography-root { - font-size: var(--env-var-font-size-medium); - color: var(--env-var-color-2); -} -.checkbox-wrapper .MuiButtonBase-root { - margin-right: var(--env-var-spacing-1); - padding: 0; -} diff --git a/Client/src/Components/Inputs/Checkbox/index.jsx b/Client/src/Components/Inputs/Checkbox/index.jsx index e454bbc6e..e7af04a64 100644 --- a/Client/src/Components/Inputs/Checkbox/index.jsx +++ b/Client/src/Components/Inputs/Checkbox/index.jsx @@ -1,5 +1,6 @@ import PropTypes from "prop-types"; import { FormControlLabel, Checkbox as MuiCheckbox } from "@mui/material"; +import { useTheme } from "@emotion/react"; import CheckboxOutline from "../../../assets/icons/checkbox-outline.svg?react"; import CheckboxFilled from "../../../assets/icons/checkbox-filled.svg?react"; @@ -16,7 +17,7 @@ import "./index.css"; * @param {boolean} [props.isDisabled] - Whether the checkbox is disabled or not. * * @returns {JSX.Element} - * + * * @example * { const sizes = { small: "14px", medium: "16px", large: "18px" }; + const theme = useTheme(); return ( diff --git a/Client/src/Components/Inputs/Field/index.css b/Client/src/Components/Inputs/Field/index.css index 3e2435694..ab1812013 100644 --- a/Client/src/Components/Inputs/Field/index.css +++ b/Client/src/Components/Inputs/Field/index.css @@ -8,24 +8,6 @@ .field .input-error { font-size: var(--env-var-font-size-medium); } -.field h3.MuiTypography-root, -.field h5.MuiTypography-root, -.field input, -.field textarea { - color: var(--env-var-color-5); -} -.field h3.MuiTypography-root { - font-weight: 500; -} -.field h3.MuiTypography-root span.field-required { - color: var(--env-var-color-19); - margin-left: 2px; -} -.field h3.MuiTypography-root span.field-optional { - opacity: 0.6; - margin-left: 4px; - font-weight: 400; -} .field h5.MuiTypography-root { position: relative; opacity: 0.8; @@ -37,24 +19,7 @@ .field .MuiInputBase-root:has(.MuiInputAdornment-root) { padding-right: var(--env-var-spacing-1-minus); } -.field - .MuiInputBase-root:has(.copy.MuiInputAdornment-root > .MuiButtonBase-root) { - padding-right: 0; -} -.field .MuiInputBase-root .copy.MuiInputAdornment-root .MuiButtonBase-root { - padding: 0 var(--env-var-spacing-1-minus); - font-size: var(--env-var-font-size-medium); - border-radius: 0; - height: 34px; -} -.field .MuiInputBase-root .copy.MuiInputAdornment-root .MuiButtonBase-root svg { - width: 20px; - height: 20px; - margin-right: 5px; -} -.field .MuiInputBase-root .MuiInputAdornment-root .MuiButtonBase-root:hover { - border-color: var(--env-var-color-29); -} + .field input { height: 100%; padding: 0 var(--env-var-spacing-1-minus); @@ -62,22 +27,6 @@ .field .MuiInputBase-root:has(textarea) { padding: var(--env-var-spacing-1-minus); } -.field .MuiOutlinedInput-root fieldset { - border-color: var(--env-var-color-29); - border-radius: var(--env-var-radius-1); -} -.field:not(:has(.Mui-disabled)):not(:has(.input-error)) - .MuiOutlinedInput-root:hover:not(:has(input:focus)):not(:has(textarea:focus)) - fieldset { - border-color: var(--env-var-color-29); -} -.field .input-error { - color: var(--env-var-color-24); - opacity: 0.8; -} -.field:has(.input-error) .MuiOutlinedInput-root fieldset { - border-color: var(--env-var-color-19); -} .register-page .field .MuiOutlinedInput-root fieldset, .register-page .field input, diff --git a/Client/src/Components/Inputs/Field/index.jsx b/Client/src/Components/Inputs/Field/index.jsx index 850598d3e..62c50eeff 100644 --- a/Client/src/Components/Inputs/Field/index.jsx +++ b/Client/src/Components/Inputs/Field/index.jsx @@ -1,4 +1,5 @@ import PropTypes from "prop-types"; +import { forwardRef, useState } from "react"; import { useTheme } from "@emotion/react"; import { IconButton, @@ -9,10 +10,7 @@ import { } from "@mui/material"; import VisibilityOff from "@mui/icons-material/VisibilityOff"; import Visibility from "@mui/icons-material/Visibility"; -import ContentCopyIcon from "@mui/icons-material/ContentCopy"; -import Button from "../../Button"; import "./index.css"; -import { forwardRef, useState } from "react"; /** * @param {Object} props @@ -23,7 +21,6 @@ import { forwardRef, useState } from "react"; * @param {boolean} [props.isRequired] - Indicates if the field is required, will display a red asterisk. * @param {boolean} [props.isOptional] - Indicates if the field is optional, will display optional text. * @param {string} [props.optionalLabel] - Optional label for the input field. - * @param {boolean} [props.hasCopy] - Indicates if the field supports copying. * @param {string} [props.autoComplete] - Autocomplete value for the input field. * @param {string} [props.placeholder] - Placeholder text for the input field. * @param {string} props.value - Value of the input field. @@ -43,7 +40,6 @@ const Field = forwardRef( isRequired, isOptional, optionalLabel, - hasCopy, autoComplete, placeholder, value, @@ -56,25 +52,54 @@ const Field = forwardRef( ) => { const theme = useTheme(); - // TODO - are we using this feature anywhere ? - const [copy, setCopy] = useState(false); - const handleCopy = () => { - setCopy(true); - setTimeout(() => setCopy(false), 1000); - }; - const [isVisible, setVisible] = useState(false); return ( - + {label && ( - + {label} - {isRequired ? * : ""} + {isRequired ? ( + + * + + ) : ( + "" + )} {isOptional ? ( - + {optionalLabel || "(optional)"} - + ) : ( "" )} @@ -92,13 +117,22 @@ const Field = forwardRef( onChange={onChange} disabled={disabled} inputRef={ref} + inputProps={{ + sx: { + color: theme.palette.text.secondary, + "&:-webkit-autofill": { + WebkitBoxShadow: `0 0 0 100px ${theme.palette.background.fill} inset`, + WebkitTextFillColor: theme.palette.text.secondary, + }, + }, + }} sx={ type === "url" ? { "& .MuiInputBase-root": { padding: 0 }, "& .MuiStack-root": { - borderTopLeftRadius: `${theme.shape.borderRadius}px`, - borderBottomLeftRadius: `${theme.shape.borderRadius}px`, + borderTopLeftRadius: theme.shape.borderRadius, + borderBottomLeftRadius: theme.shape.borderRadius, }, } : {} @@ -110,65 +144,53 @@ const Field = forwardRef( alignItems="center" height="100%" sx={{ - borderRight: `solid 1px ${theme.palette.section.borderColor}`, - backgroundColor: "#f9f9fa", - pl: theme.gap.medium, + borderRight: `solid 1px ${theme.palette.border.dark}`, + backgroundColor: theme.palette.background.accent, + pl: theme.spacing(6), }} > - + {https ? "https" : "http"}:// ), - endAdornment: - type === "password" ? ( - - setVisible((show) => !show)} - tabIndex={-1} - sx={{ - color: theme.palette.section.borderColor, - padding: `calc(${theme.gap.xs} / 2)`, - "&:focus": { - outline: "none", - }, - "& .MuiTouchRipple-root": { - pointerEvents: "none", - display: "none", - }, - }} - > - {!isVisible ? : } - - - ) : ( - hasCopy && ( - - + - - + + loading={isLoading} + > + Delete account + @@ -406,17 +415,22 @@ const ProfilePanel = () => { left: "50%", transform: "translate(-50%, -50%)", width: 450, - bgcolor: "white", - border: "solid 1px #f2f2f2", - borderRadius: `${theme.shape.borderRadius}px`, + bgcolor: theme.palette.background.main, + border: 1, + borderColor: theme.palette.border.light, + borderRadius: theme.shape.borderRadius, boxShadow: 24, - p: "30px", + p: theme.spacing(15), "&:focus": { outline: "none", }, }} > - + Upload Image { ) : ( "" )} - + + diff --git a/Client/src/Components/TabPanels/Account/TeamPanel.jsx b/Client/src/Components/TabPanels/Account/TeamPanel.jsx index a0393977d..8c4630b19 100644 --- a/Client/src/Components/TabPanels/Account/TeamPanel.jsx +++ b/Client/src/Components/TabPanels/Account/TeamPanel.jsx @@ -2,6 +2,7 @@ import { useTheme } from "@emotion/react"; import TabPanel from "@mui/lab/TabPanel"; import { Box, + Button, ButtonGroup, Divider, IconButton, @@ -10,8 +11,6 @@ import { TextField, Typography, } from "@mui/material"; -import ButtonSpinner from "../../ButtonSpinner"; -import Button from "../../Button"; import { useEffect, useState } from "react"; import EditSvg from "../../../assets/icons/edit.svg?react"; import Field from "../../Inputs/Field"; @@ -22,6 +21,7 @@ import { useSelector } from "react-redux"; import BasicTable from "../../BasicTable"; import Remove from "../../../assets/icons/trash-bin.svg?react"; import Select from "../../Inputs/Select"; +import LoadingButton from "@mui/lab/LoadingButton"; /** * TeamPanel component manages the organization and team members, @@ -32,6 +32,12 @@ import Select from "../../Inputs/Select"; */ const TeamPanel = () => { + const roleMap = { + superadmin: "Super admin", + admin: "Admin", + user: "Team member", + }; + const theme = useTheme(); const { authToken, user } = useSelector((state) => state.auth); @@ -67,7 +73,14 @@ const TeamPanel = () => { useEffect(() => { let team = members; if (filter !== "all") - team = members.filter((member) => member.role.includes(filter)); + team = members.filter((member) => { + if (filter === "admin") { + return ( + member.role.includes("admin") || member.role.includes("superadmin") + ); + } + return member.role.includes(filter); + }); const data = { cols: [ @@ -78,6 +91,7 @@ const TeamPanel = () => { // { id: 4, name: "ACTION" }, ], rows: team?.map((member, idx) => { + const roles = member.role.map((role) => roleMap[role]).join(","); return { id: member._id, data: [ @@ -85,12 +99,10 @@ const TeamPanel = () => { id: idx, data: ( - + {member.firstName + " " + member.lastName} - + Created {new Date(member.createdAt).toLocaleDateString()} @@ -100,7 +112,7 @@ const TeamPanel = () => { { // TODO - Add select dropdown id: idx + 2, - data: member.role.includes("admin") ? "Administrator" : "Member", + data: roles, }, // FEATURE STILL TO BE IMPLEMENTED // { @@ -198,7 +210,18 @@ const TeamPanel = () => { }; return ( - + {/* FEATURE STILL TO BE IMPLEMENTED */} {/* @@ -229,7 +252,7 @@ const TeamPanel = () => { }, }} inputProps={{ - sx: { textAlign: "end", padding: theme.gap.small }, + sx: { textAlign: "end", padding: theme.spacing(4) }, }} /> { disablePortal > { When you add a new team member, they will get access to all monitors. @@ -360,28 +379,28 @@ const TeamPanel = () => { })) } items={[ - { _id: "admin", name: "admin" }, - { _id: "user", name: "user" }, + { _id: "admin", name: "Admin" }, + { _id: "user", name: "User" }, ]} /> - + + > + Send invite + diff --git a/Client/src/Components/Tooltips/TooltipWithTail/TooltipWithTail.jsx b/Client/src/Components/Tooltips/TooltipWithTail/TooltipWithTail.jsx deleted file mode 100644 index 1b1fb8b4d..000000000 --- a/Client/src/Components/Tooltips/TooltipWithTail/TooltipWithTail.jsx +++ /dev/null @@ -1,45 +0,0 @@ -import "./tooltipWithTail.css"; -import React from "react"; -import { styled } from "@mui/material/styles"; -import Tooltip, { tooltipClasses } from "@mui/material/Tooltip"; -import HelpOutlineIcon from "@mui/icons-material/HelpOutline"; - -const CustomizedTooltip = styled( - ({ className, placement, arrow, ...props }) => ( - - ) -)(({ theme }) => ({ - [`& .${tooltipClasses.tooltip}`]: { - backgroundColor: "black", - }, - [`& .${tooltipClasses.arrow}`]: { - color: "black", - }, -})); - -function TooltipWithTail({ placement, arrow = false, title, text }) { - return ( -
- - {title &&
{title}
} -
{text}
- - } - className="tooltip-holder" - > - -
-
- ); -} - -export default TooltipWithTail; diff --git a/Client/src/Components/Tooltips/TooltipWithTail/tooltipWithTail.css b/Client/src/Components/Tooltips/TooltipWithTail/tooltipWithTail.css deleted file mode 100644 index 3efe67643..000000000 --- a/Client/src/Components/Tooltips/TooltipWithTail/tooltipWithTail.css +++ /dev/null @@ -1,49 +0,0 @@ -:root { - --env-var-color-1: #101828; - --env-var-color-2: #475467; - --env-var-color-3: #1570ef; - - --env-var-color-4: #d0d5dd; - --env-var-color-5: #344054; - --env-var-color-6: #eaecf0; - --env-var-color-7: #7f56d9; - --env-var-color-8: #fff; - --env-var-color-9: #f2f2f2; - --env-var-color-10: #175cd3; - - --env-var-radius-1: 4px; - --env-var-radius-2: 8px; - - --env-var-width-1: 100vw; - --env-var-width-2: 360px; - - --env-var-height-1: 100vh; - - --env-var-spacing-1: 12px; - --env-var-spacing-2: 24px; - --env-var-spacing-3: 32px; - --env-var-spacing-4: 40px; - - --env-var-font-size-small: 11px; - --env-var-font-size-medium: 13px; - --env-var-font-size-medium-plus: 14px; - --env-var-font-size-large: 16px; - --env-var-font-size-xlarge: 30px; - - --env-var-img-width-1: 20px; - - --env-var-default-1: 24px; - --env-var-default-2: 40px; -} - -.tooltip-title { - font-size: var(--env-var-font-size-medium); - font-weight: bold; - margin: 5px; -} - -.tooltip-description { - font-size: var(--env-var-font-size-medium); - color: var(--env-var-color-4); - margin: 5px; -} diff --git a/Client/src/Features/Auth/authSlice.js b/Client/src/Features/Auth/authSlice.js index fa1f0471e..f12758715 100644 --- a/Client/src/Features/Auth/authSlice.js +++ b/Client/src/Features/Auth/authSlice.js @@ -1,6 +1,7 @@ import { networkService } from "../../main"; import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import { jwtDecode } from "jwt-decode"; +import axios from "axios"; const initialState = { isLoading: false, @@ -57,9 +58,8 @@ export const update = createAsyncThunk( form.password && fd.append("password", form.password); form.newPassword && fd.append("newPassword", form.newPassword); if (form.file && form.file !== "") { - const imageResult = await networkService.get(form.file, { + const imageResult = await axios.get(form.file, { responseType: "blob", - baseURL: "", }); fd.append("profileImage", imageResult.data); } diff --git a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js index 22e2b3324..e32d673cc 100644 --- a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js +++ b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js @@ -28,14 +28,14 @@ export const createPageSpeed = createAsyncThunk( } ); -export const getPageSpeedByUserId = createAsyncThunk( - "pageSpeedMonitors/getPageSpeedByUserId", +export const getPageSpeedByTeamId = createAsyncThunk( + "pageSpeedMonitors/getPageSpeedByTeamId", async (token, thunkApi) => { const user = jwtDecode(token); try { - const res = await networkService.getMonitorsByUserId( + const res = await networkService.getMonitorsByTeamId( token, - user._id, + user.teamId, 25, ["pagespeed"], null, @@ -127,15 +127,15 @@ const pageSpeedMonitorSlice = createSlice({ // Monitors by userId // ***************************************************** - .addCase(getPageSpeedByUserId.pending, (state) => { + .addCase(getPageSpeedByTeamId.pending, (state) => { state.isLoading = true; }) - .addCase(getPageSpeedByUserId.fulfilled, (state, action) => { + .addCase(getPageSpeedByTeamId.fulfilled, (state, action) => { state.isLoading = false; state.success = action.payload.msg; state.monitors = action.payload.data; }) - .addCase(getPageSpeedByUserId.rejected, (state, action) => { + .addCase(getPageSpeedByTeamId.rejected, (state, action) => { state.isLoading = false; state.success = false; state.msg = action.payload diff --git a/Client/src/Features/UI/uiSlice.js b/Client/src/Features/UI/uiSlice.js index ebea20bf1..947055ddc 100644 --- a/Client/src/Features/UI/uiSlice.js +++ b/Client/src/Features/UI/uiSlice.js @@ -4,7 +4,7 @@ import { createSlice } from "@reduxjs/toolkit"; // Add more settings as needed (e.g., theme preferences, user settings) const initialState = { monitors: { - rowsPerPage: 5, + rowsPerPage: 10, }, team: { rowsPerPage: 5, @@ -12,6 +12,8 @@ const initialState = { sidebar: { collapsed: false, }, + mode: "light", + greeting: { index: 0, lastUpdate: null }, }; const uiSlice = createSlice({ @@ -19,13 +21,24 @@ const uiSlice = createSlice({ initialState, reducers: { setRowsPerPage: (state, action) => { - state[action.payload.table].rowsPerPage = action.payload.value; + const { table, value } = action.payload; + if (state[table]) { + state[table].rowsPerPage = value; + } }, toggleSidebar: (state) => { state.sidebar.collapsed = !state.sidebar.collapsed; }, + setMode: (state, action) => { + state.mode = action.payload; + }, + setGreeting(state, action) { + state.greeting.index = action.payload.index; + state.greeting.lastUpdate = action.payload.lastUpdate; + }, }, }); export default uiSlice.reducer; -export const { setRowsPerPage, toggleSidebar } = uiSlice.actions; +export const { setRowsPerPage, toggleSidebar, setMode, setGreeting } = + uiSlice.actions; diff --git a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js index 9ab7d385b..2c381812c 100644 --- a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js +++ b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js @@ -28,14 +28,34 @@ export const createUptimeMonitor = createAsyncThunk( } ); -export const getUptimeMonitorsByUserId = createAsyncThunk( - "montiors/getMonitorsByUserId", +export const getUptimeMonitorById = createAsyncThunk( + "monitors/getMonitorById", + async (data, thunkApi) => { + try { + const { authToken, monitorId } = data; + const res = await networkService.getMonitorByid(authToken, monitorId); + return res.data; + } catch (error) { + if (error.response && error.response.data) { + return thunkApi.rejectWithValue(error.response.data); + } + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); + } + } +); + +export const getUptimeMonitorsByTeamId = createAsyncThunk( + "monitors/getMonitorsByTeamId", async (token, thunkApi) => { const user = jwtDecode(token); try { - const res = await networkService.getMonitorsByUserId( + const res = await networkService.getMonitorsByTeamId( token, - user._id, + user.teamId, 25, ["http", "ping"], null, @@ -109,6 +129,46 @@ export const deleteUptimeMonitor = createAsyncThunk( } ); +export const pauseUptimeMonitor = createAsyncThunk( + "monitors/pauseMonitor", + async (data, thunkApi) => { + try { + const { authToken, monitorId } = data; + const res = await networkService.pauseMonitorById(authToken, monitorId); + return res.data; + } catch (error) { + if (error.response && error.response.data) { + return thunkApi.rejectWithValue(error.response.data); + } + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); + } + } +); + +export const deleteMonitorChecksByTeamId = createAsyncThunk( + "monitors/deleteChecksByTeamId", + async (data, thunkApi) => { + try { + const { authToken, teamId } = data; + const res = await networkService.deleteChecksByTeamId(authToken, teamId); + return res.data; + } catch (error) { + if (error.response && error.response.data) { + return thunkApi.rejectWithValue(error.response.data); + } + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); + } + } +); + const uptimeMonitorsSlice = createSlice({ name: "uptimeMonitors", initialState, @@ -126,15 +186,15 @@ const uptimeMonitorsSlice = createSlice({ // Monitors by userId // ***************************************************** - .addCase(getUptimeMonitorsByUserId.pending, (state) => { + .addCase(getUptimeMonitorsByTeamId.pending, (state) => { state.isLoading = true; }) - .addCase(getUptimeMonitorsByUserId.fulfilled, (state, action) => { + .addCase(getUptimeMonitorsByTeamId.fulfilled, (state, action) => { state.isLoading = false; state.success = action.payload.msg; state.monitors = action.payload.data; }) - .addCase(getUptimeMonitorsByUserId.rejected, (state, action) => { + .addCase(getUptimeMonitorsByTeamId.rejected, (state, action) => { state.isLoading = false; state.success = false; state.msg = action.payload @@ -160,7 +220,24 @@ const uptimeMonitorsSlice = createSlice({ ? action.payload.msg : "Failed to create uptime monitor"; }) - + // ***************************************************** + // Get Monitor By Id + // ***************************************************** + .addCase(getUptimeMonitorById.pending, (state) => { + state.isLoading = true; + }) + .addCase(getUptimeMonitorById.fulfilled, (state, action) => { + state.isLoading = false; + state.success = action.payload.success; + state.msg = action.payload.msg; + }) + .addCase(getUptimeMonitorById.rejected, (state, action) => { + state.isLoading = false; + state.success = false; + state.msg = action.payload + ? action.payload.msg + : "Failed to pause uptime monitor"; + }) // ***************************************************** // update Monitor // ***************************************************** @@ -197,6 +274,42 @@ const uptimeMonitorsSlice = createSlice({ state.msg = action.payload ? action.payload.msg : "Failed to delete uptime monitor"; + }) + // ***************************************************** + // Delete Monitor checks by Team ID + // ***************************************************** + .addCase(deleteMonitorChecksByTeamId.pending, (state) => { + state.isLoading = true; + }) + .addCase(deleteMonitorChecksByTeamId.fulfilled, (state, action) => { + state.isLoading = false; + state.success = action.payload.success; + state.msg = action.payload.msg; + }) + .addCase(deleteMonitorChecksByTeamId.rejected, (state, action) => { + state.isLoading = false; + state.success = false; + state.msg = action.payload + ? action.payload.msg + : "Failed to delete monitor checks"; + }) + // ***************************************************** + // Pause Monitor + // ***************************************************** + .addCase(pauseUptimeMonitor.pending, (state) => { + state.isLoading = true; + }) + .addCase(pauseUptimeMonitor.fulfilled, (state, action) => { + state.isLoading = false; + state.success = action.payload.success; + state.msg = action.payload.msg; + }) + .addCase(pauseUptimeMonitor.rejected, (state, action) => { + state.isLoading = false; + state.success = false; + state.msg = action.payload + ? action.payload.msg + : "Failed to pause uptime monitor"; }); }, }); diff --git a/Client/src/HOC/withAdminCheck.jsx b/Client/src/HOC/withAdminCheck.jsx index 30a767f36..097f224e9 100644 --- a/Client/src/HOC/withAdminCheck.jsx +++ b/Client/src/HOC/withAdminCheck.jsx @@ -10,7 +10,7 @@ const withAdminCheck = (WrappedComponent) => { useEffect(() => { networkService - .doesAdminExist() + .doesSuperAdminExist() .then((response) => { if (response.data.data === true) { navigate("/login"); @@ -20,7 +20,7 @@ const withAdminCheck = (WrappedComponent) => { logger.error(error); }); }, [navigate]); - return ; + return ; }; const wrappedComponentName = WrappedComponent.displayName || WrappedComponent.name || "Component"; diff --git a/Client/src/HOC/withAdminProp.jsx b/Client/src/HOC/withAdminProp.jsx new file mode 100644 index 000000000..6910ba5ea --- /dev/null +++ b/Client/src/HOC/withAdminProp.jsx @@ -0,0 +1,20 @@ +import { useSelector } from "react-redux"; + +const withAdminProp = (WrappedComponent) => { + const WithAdminProp = (props) => { + const { user } = useSelector((state) => state.auth); + const isAdmin = + (user?.role?.includes("admin") ?? false) || + (user?.role?.includes("superadmin") ?? false); + + return ; + }; + + const wrappedComponentName = + WrappedComponent.displayName || WrappedComponent.name || "Component"; + WithAdminProp.displayName = `WithAdminProp(${wrappedComponentName})`; + + return WithAdminProp; +}; + +export default withAdminProp; diff --git a/Client/src/Layouts/HomeLayout/index.css b/Client/src/Layouts/HomeLayout/index.css index e85a6362b..1434ff9d9 100644 --- a/Client/src/Layouts/HomeLayout/index.css +++ b/Client/src/Layouts/HomeLayout/index.css @@ -1,5 +1,5 @@ #root:has(.home-layout) { - background-color: var(--env-var-color-30); + background-color: var(--secondary-bg); } .home-layout { @@ -19,12 +19,6 @@ height: calc(100vh - var(--env-var-spacing-2) * 2); max-width: var(--env-var-side-bar-width); - - border: 1px solid var(--env-var-color-6); - border-radius: var(--env-var-radius-1); - background-color: var(--env-var-color-8); - - padding: var(--env-var-spacing-1) 0; } .home-layout > div { @@ -32,19 +26,11 @@ 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%); + transform: translate(-50%, -50%); z-index: 0; width: 100%; diff --git a/Client/src/Layouts/HomeLayout/index.jsx b/Client/src/Layouts/HomeLayout/index.jsx index 81cb2fd9c..92d763f0c 100644 --- a/Client/src/Layouts/HomeLayout/index.jsx +++ b/Client/src/Layouts/HomeLayout/index.jsx @@ -1,14 +1,19 @@ import Sidebar from "../../Components/Sidebar"; import { Outlet } from "react-router"; import { Box } from "@mui/material"; +import { useTheme } from "@emotion/react"; import "./index.css"; const HomeLayout = () => { + const theme = useTheme(); + return ( - - - + + + + + ); }; diff --git a/Client/src/Pages/Account/index.css b/Client/src/Pages/Account/index.css index 594b7b839..24b47d3e9 100644 --- a/Client/src/Pages/Account/index.css +++ b/Client/src/Pages/Account/index.css @@ -6,12 +6,6 @@ .account .MuiSelect-select { font-size: var(--env-var-font-size-medium); } -.account h1.MuiTypography-root, -.account p.MuiTypography-root, -.account input, -.account .MuiSelect-select { - color: var(--env-var-color-2); -} .account h1.MuiTypography-root { font-weight: 600; } @@ -19,10 +13,6 @@ padding: 0; margin-top: 50px; } -.account .MuiDivider-root, -.account .MuiButtonGroup-root button { - border-color: var(--env-var-color-16); -} .account button:not(.MuiIconButton-root) { height: 34px; } @@ -43,9 +33,6 @@ top: 100%; } -.account .MuiTable-root .MuiTableBody-root .MuiTableCell-root { - color: var(--env-var-color-25); -} .account .MuiTableBody-root .MuiTableCell-root { padding: var(--env-var-spacing-1-plus) var(--env-var-spacing-2); } diff --git a/Client/src/Pages/Account/index.jsx b/Client/src/Pages/Account/index.jsx index 2fed926b8..fe7b19655 100644 --- a/Client/src/Pages/Account/index.jsx +++ b/Client/src/Pages/Account/index.jsx @@ -25,23 +25,27 @@ const Account = ({ open = "profile" }) => { let tabList = ["Profile", "Password", "Team"]; const { user } = useSelector((state) => state.auth); - if (!user.role.includes("admin")) tabList = ["Profile", "Password"]; + const requiredRoles = ["superadmin", "admin"]; + const hideTeams = !requiredRoles.some((role) => user.role.includes(role)); + if (hideTeams) { + tabList = ["Profile", "Password"]; + } return ( @@ -52,15 +56,15 @@ const Account = ({ open = "profile" }) => { key={index} value={label.toLowerCase()} sx={{ - fontSize: "13px", - color: theme.palette.secondary.main, + fontSize: 13, + color: theme.palette.text.tertiary, textTransform: "none", minWidth: "fit-content", minHeight: 0, - paddingLeft: "0", - paddingY: theme.gap.small, + paddingLeft: 0, + paddingY: theme.spacing(4), fontWeight: 400, - marginRight: theme.gap.ml, + marginRight: theme.spacing(8), "&:focus": { outline: "none", }, @@ -71,7 +75,7 @@ const Account = ({ open = "profile" }) => { - {user.role.includes("admin") && } + {!hideTeams && } ); diff --git a/Client/src/Pages/Auth/CheckEmail.jsx b/Client/src/Pages/Auth/CheckEmail.jsx index 4a0daf68a..88bc1983c 100644 --- a/Client/src/Pages/Auth/CheckEmail.jsx +++ b/Client/src/Pages/Auth/CheckEmail.jsx @@ -1,11 +1,10 @@ -import { Box, Stack, Typography } from "@mui/material"; +import { Box, Button, Stack, Typography } from "@mui/material"; import { useEffect, useState } from "react"; import { useTheme } from "@emotion/react"; import { useDispatch } from "react-redux"; import { useNavigate } from "react-router"; import { createToast } from "../../Utils/toastUtils"; import { forgotPassword } from "../../Features/Auth/authSlice"; -import Button from "../../Components/Button"; import background from "../../assets/Images/background_pattern_decorative.png"; import EmailIcon from "../../assets/icons/email.svg?react"; import Logo from "../../assets/icons/bwu-icon.svg?react"; @@ -85,7 +84,21 @@ const CheckEmail = () => { }; return ( - + { BlueWave Uptime @@ -104,31 +117,31 @@ const CheckEmail = () => { maxWidth={600} flex={1} justifyContent="center" - px={{ xs: theme.gap.large, lg: theme.gap.xl }} - pb={theme.gap.xl} + px={{ xs: theme.spacing(12), lg: theme.spacing(20) }} + pb={theme.spacing(20)} mx="auto" sx={{ "& > .MuiStack-root": { border: 1, - borderRadius: theme.shape.borderRadius, - borderColor: theme.palette.otherColors.graishWhite, - backgroundColor: theme.palette.otherColors.white, + borderRadius: theme.spacing(5), + borderColor: theme.palette.border.light, + backgroundColor: theme.palette.background.main, padding: { - xs: theme.gap.large, - sm: theme.gap.xl, + xs: theme.spacing(12), + sm: theme.spacing(20), }, }, }} > Check your email - + We sent a password reset link to{" "} {email || "username@email.com"} @@ -136,15 +149,17 @@ const CheckEmail = () => { + Didn't receive the email?{" "} { - + Go back to — diff --git a/Client/src/Pages/Auth/ForgotPassword.jsx b/Client/src/Pages/Auth/ForgotPassword.jsx index 06afb05fa..338da645c 100644 --- a/Client/src/Pages/Auth/ForgotPassword.jsx +++ b/Client/src/Pages/Auth/ForgotPassword.jsx @@ -7,10 +7,10 @@ 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 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 LoadingButton from "@mui/lab/LoadingButton"; import "./index.css"; const ForgotPassword = () => { @@ -87,7 +87,21 @@ const ForgotPassword = () => { }; return ( - + { BlueWave Uptime @@ -106,24 +120,24 @@ const ForgotPassword = () => { maxWidth={600} flex={1} justifyContent="center" - px={{ xs: theme.gap.large, lg: theme.gap.xl }} - pb={theme.gap.xl} + px={{ xs: theme.spacing(12), lg: theme.spacing(20) }} + pb={theme.spacing(20)} mx="auto" sx={{ "& > .MuiStack-root": { border: 1, - borderRadius: theme.shape.borderRadius, - borderColor: theme.palette.otherColors.graishWhite, - backgroundColor: theme.palette.otherColors.white, + borderRadius: theme.spacing(5), + borderColor: theme.palette.border.light, + backgroundColor: theme.palette.background.main, padding: { - xs: theme.gap.large, - sm: theme.gap.xl, + xs: theme.spacing(12), + sm: theme.spacing(20), }, }, }} > @@ -134,39 +148,46 @@ const ForgotPassword = () => { No worries, we'll send you reset instructions. - -
- - - + + + + Send instructions +
- + Go back to — diff --git a/Client/src/Pages/Auth/Login.jsx b/Client/src/Pages/Auth/Login.jsx index b962fbb9a..c8946b7fb 100644 --- a/Client/src/Pages/Auth/Login.jsx +++ b/Client/src/Pages/Auth/Login.jsx @@ -1,12 +1,11 @@ import { useState, useEffect, useRef } from "react"; import { useNavigate } from "react-router-dom"; -import { Box, Stack, Typography } from "@mui/material"; +import { Box, Button, Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import { credentials } from "../../Validation/validation"; import { login } from "../../Features/Auth/authSlice"; import { useDispatch, useSelector } from "react-redux"; import { createToast } from "../../Utils/toastUtils"; -import Button from "../../Components/Button"; import { networkService } from "../../main"; import Field from "../../Components/Inputs/Field"; import background from "../../assets/Images/background_pattern_decorative.png"; @@ -30,7 +29,7 @@ const LandingPage = ({ onContinue }) => { return ( <> @@ -40,17 +39,22 @@ const LandingPage = ({ onContinue }) => { @@ -64,6 +68,11 @@ const LandingPage = ({ onContinue }) => { "noreferrer" ); }} + sx={{ + "&:hover": { + color: theme.palette.text.tertiary, + }, + }} > Terms of Service {" "} @@ -77,6 +86,11 @@ const LandingPage = ({ onContinue }) => { "noreferrer" ); }} + sx={{ + "&:hover": { + color: theme.palette.text.tertiary, + }, + }} > Privacy Policy.
@@ -114,12 +128,15 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => { return ( <> - + Log In Enter your email address - +
{ @@ -204,12 +223,16 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => { return ( <> - + Log In Enter your password - + { - - + + Forgot password? @@ -301,7 +339,7 @@ const Login = () => { return; } networkService - .doesAdminExist() + .doesSuperAdminExist() .then((response) => { if (response.data.data === false) { navigate("/register"); @@ -393,7 +431,21 @@ const Login = () => { }; return ( - + { BlueWave Uptime @@ -412,18 +464,18 @@ const Login = () => { maxWidth={600} flex={1} justifyContent="center" - px={{ xs: theme.gap.large, lg: theme.gap.xl }} - pb={theme.gap.xl} + px={{ xs: theme.spacing(12), lg: theme.spacing(20) }} + pb={theme.spacing(20)} mx="auto" sx={{ "& > .MuiStack-root": { border: 1, - borderRadius: theme.shape.borderRadius, - borderColor: theme.palette.otherColors.graishWhite, - backgroundColor: theme.palette.otherColors.white, + borderRadius: theme.spacing(5), + borderColor: theme.palette.border.light, + backgroundColor: theme.palette.background.main, padding: { - xs: theme.gap.large, - sm: theme.gap.xl, + xs: theme.spacing(12), + sm: theme.spacing(20), }, }, }} @@ -450,13 +502,14 @@ const Login = () => { ) )} - + Don't have an account? — { navigate("/register"); }} diff --git a/Client/src/Pages/Auth/NewPasswordConfirmed.jsx b/Client/src/Pages/Auth/NewPasswordConfirmed.jsx index be6aeb9b5..7be6d325d 100644 --- a/Client/src/Pages/Auth/NewPasswordConfirmed.jsx +++ b/Client/src/Pages/Auth/NewPasswordConfirmed.jsx @@ -1,10 +1,9 @@ -import { Box, Stack, Typography } from "@mui/material"; +import { Box, Button, 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"; @@ -22,7 +21,21 @@ const NewPasswordConfirmed = () => { }; return ( - + { BlueWave Uptime @@ -41,51 +54,54 @@ const NewPasswordConfirmed = () => { maxWidth={600} flex={1} justifyContent="center" - px={{ xs: theme.gap.large, lg: theme.gap.xl }} - pb={theme.gap.xl} + px={{ xs: theme.spacing(12), lg: theme.spacing(20) }} + pb={theme.spacing(20)} mx="auto" sx={{ "& > .MuiStack-root": { border: 1, - borderRadius: theme.shape.borderRadius, - borderColor: theme.palette.otherColors.graishWhite, - backgroundColor: theme.palette.otherColors.white, + borderRadius: theme.spacing(5), + borderColor: theme.palette.border.light, + backgroundColor: theme.palette.background.main, padding: { - xs: theme.gap.large, - sm: theme.gap.xl, + xs: theme.spacing(12), + sm: theme.spacing(20), }, }, }} > Password reset - + Your password has been successfully reset. Click below to log in magically. - + Go back to — diff --git a/Client/src/Pages/Auth/Register/Register.jsx b/Client/src/Pages/Auth/Register/Register.jsx index 4680c1821..e454b355f 100644 --- a/Client/src/Pages/Auth/Register/Register.jsx +++ b/Client/src/Pages/Auth/Register/Register.jsx @@ -2,7 +2,7 @@ import PropTypes from "prop-types"; import { useState, useEffect, useRef } from "react"; import { useNavigate } from "react-router-dom"; import { useTheme } from "@emotion/react"; -import { Box, Stack, Typography } from "@mui/material"; +import { Box, Button, Stack, Typography } from "@mui/material"; import { useDispatch } from "react-redux"; import { credentials } from "../../../Validation/validation"; import { createToast } from "../../../Utils/toastUtils"; @@ -13,49 +13,53 @@ import Logo from "../../../assets/icons/bwu-icon.svg?react"; import Mail from "../../../assets/icons/mail.svg?react"; import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; import Check from "../../../Components/Check/Check"; -import Button from "../../../Components/Button"; import Field from "../../../Components/Inputs/Field"; import { networkService } from "../../../main"; import "../index.css"; -import { logger } from "../../../Utils/Logger"; /** * Displays the initial landing page. * * @param {Object} props - * @param {boolean} props.isAdmin - Whether the user is creating and admin account + * @param {boolean} props.isSuperAdmin - Whether the user is creating and admin account * @param {Function} props.onContinue - Callback function to handle "Continue with Email" button click. * @returns {JSX.Element} */ -const LandingPage = ({ isAdmin, onSignup }) => { +const LandingPage = ({ isSuperAdmin, onSignup }) => { const theme = useTheme(); return ( <> Sign Up - Create your {isAdmin ? "admin " : ""}account to get started. + Create your {isSuperAdmin ? "Super admin " : ""}account to get + started. @@ -69,6 +73,11 @@ const LandingPage = ({ isAdmin, onSignup }) => { "noreferrer" ); }} + sx={{ + "&:hover": { + color: theme.palette.text.tertiary, + }, + }} > Terms of Service {" "} @@ -82,6 +91,11 @@ const LandingPage = ({ isAdmin, onSignup }) => { "noreferrer" ); }} + sx={{ + "&:hover": { + color: theme.palette.text.tertiary, + }, + }} > Privacy Policy. @@ -93,7 +107,7 @@ const LandingPage = ({ isAdmin, onSignup }) => { }; LandingPage.propTypes = { - isAdmin: PropTypes.bool, + isSuperAdmin: PropTypes.bool, onSignup: PropTypes.func, }; @@ -120,13 +134,22 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => { return ( <> - + Sign Up Enter your personal details - + { error={errors.firstName} ref={inputRef} /> - -
+ + { onChange={onChange} error={errors.lastName} /> - +
@@ -211,13 +243,22 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => { return ( <> - + Sign Up Enter your email address -
+ { error={errors.email} ref={inputRef} /> - +
@@ -292,13 +336,28 @@ const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => { return ( <> - + Sign Up Create your password - -
+ + { error={errors.password} ref={inputRef} /> - -
+ + { onChange={onChange} error={errors.confirm} /> - +
{
@@ -422,7 +489,7 @@ StepThree.propTypes = { onBack: PropTypes.func, }; -const Register = ({ isAdmin }) => { +const Register = ({ isSuperAdmin }) => { const dispatch = useDispatch(); const navigate = useNavigate(); const { token } = useParams(); @@ -443,6 +510,7 @@ const Register = ({ isAdmin }) => { password: "", confirm: "", role: [], + teamId: "", }); const [errors, setErrors] = useState({}); const [step, setStep] = useState(0); @@ -452,15 +520,16 @@ const Register = ({ isAdmin }) => { if (token !== undefined) { try { const res = await networkService.verifyInvitationToken(token); - const { role, email } = res.data.data; - setForm({ ...form, email, role }); + const invite = res.data.data; + const { role, email, teamId } = invite; + setForm({ ...form, email, role, teamId }); } catch (error) { - logger.error(error); + navigate("/register", { replace: true }); } } }; fetchInvite(); - }, [token, form]); + }, []); /** * Validates the form data against the validation schema. @@ -493,7 +562,6 @@ const Register = ({ isAdmin }) => { const handleStepOne = async (e) => { e.preventDefault(); - let error = validateForm({ firstName: form.firstName, lastName: form.lastName, @@ -525,7 +593,10 @@ const Register = ({ isAdmin }) => { const handleStepThree = async (e) => { e.preventDefault(); - let registerForm = { ...form, role: isAdmin ? ["admin"] : form.role }; + let registerForm = { + ...form, + role: isSuperAdmin ? ["superadmin"] : form.role, + }; let error = validateForm(registerForm, { context: { password: form.password }, }); @@ -580,7 +651,21 @@ const Register = ({ isAdmin }) => { }; return ( - + { BlueWave Uptime @@ -599,24 +684,27 @@ const Register = ({ isAdmin }) => { maxWidth={600} flex={1} justifyContent="center" - px={{ xs: theme.gap.large, lg: theme.gap.xl }} - pb={theme.gap.xl} + px={{ xs: theme.spacing(12), lg: theme.spacing(20) }} + pb={theme.spacing(20)} mx="auto" sx={{ "& > .MuiStack-root": { border: 1, - borderRadius: theme.shape.borderRadius, - borderColor: theme.palette.otherColors.graishWhite, - backgroundColor: theme.palette.otherColors.white, + borderRadius: theme.spacing(5), + borderColor: theme.palette.border.light, + backgroundColor: theme.palette.background.main, padding: { - xs: theme.gap.large, - sm: theme.gap.xl, + xs: theme.spacing(12), + sm: theme.spacing(20), }, }, }} > {step === 0 ? ( - setStep(1)} /> + setStep(1)} + /> ) : step === 1 ? ( { "" )} - + Already have an account? — { navigate("/login"); }} - sx={{ userSelect: "none" }} + sx={{ userSelect: "none", color: theme.palette.primary.main }} > Log In @@ -664,6 +752,6 @@ const Register = ({ isAdmin }) => { ); }; Register.propTypes = { - isAdmin: PropTypes.bool, + isSuperAdmin: PropTypes.bool, }; export default Register; diff --git a/Client/src/Pages/Auth/SetNewPassword.jsx b/Client/src/Pages/Auth/SetNewPassword.jsx index fab448ffa..a01e9bd51 100644 --- a/Client/src/Pages/Auth/SetNewPassword.jsx +++ b/Client/src/Pages/Auth/SetNewPassword.jsx @@ -8,11 +8,11 @@ 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 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 LoadingButton from "@mui/lab/LoadingButton"; import "./index.css"; const SetNewPassword = () => { @@ -104,7 +104,21 @@ const SetNewPassword = () => { }; return ( - + { BlueWave Uptime @@ -123,24 +137,24 @@ const SetNewPassword = () => { maxWidth={600} flex={1} justifyContent="center" - px={{ xs: theme.gap.large, lg: theme.gap.xl }} - pb={theme.gap.large} + px={{ xs: theme.spacing(12), lg: theme.spacing(20) }} + pb={theme.spacing(12)} mx="auto" sx={{ "& > .MuiStack-root": { border: 1, - borderRadius: theme.shape.borderRadius, - borderColor: theme.palette.otherColors.graishWhite, - backgroundColor: theme.palette.otherColors.white, + borderRadius: theme.spacing(5), + borderColor: theme.palette.border.light, + backgroundColor: theme.palette.background.main, padding: { - xs: theme.gap.large, - sm: theme.gap.xl, + xs: theme.spacing(12), + sm: theme.spacing(20), }, }, }} > @@ -151,8 +165,21 @@ const SetNewPassword = () => { Your new password must be different to previously used passwords.
- -
+ + { onChange={handleChange} error={errors.password} /> - -
+ + { onChange={handleChange} error={errors.confirm} /> - - +
+ @@ -234,21 +266,24 @@ const SetNewPassword = () => { />
- + > + Reset password +
- + Go back to — navigate("/login")} sx={{ userSelect: "none" }} > diff --git a/Client/src/Pages/Auth/index.css b/Client/src/Pages/Auth/index.css index 979ae191c..33280e8f4 100644 --- a/Client/src/Pages/Auth/index.css +++ b/Client/src/Pages/Auth/index.css @@ -5,24 +5,14 @@ .auth h1 { font-size: var(--env-var-font-size-xlarge); font-weight: 600; - color: var(--env-var-color-3); } .auth button:not(.MuiIconButton-root), .auth p, -.auth .field input, .auth span { font-size: var(--env-var-font-size-medium-plus); } -.auth p { - color: var(--env-var-color-2-light); -} -.auth .field h3.MuiTypography-root, -.auth .field input { - color: var(--env-var-color-5); -} .auth p + span { - color: var(--env-var-color-3); opacity: 0.8; cursor: pointer; transition: opacity 300ms ease-in; @@ -34,7 +24,6 @@ transition: all 200ms; } .auth p > span:not(.email-sent-to):hover { - color: var(--env-var-color-2); text-underline-offset: 4px; } .auth p + span:hover { @@ -86,11 +75,12 @@ background-repeat: no-repeat; } -.forgot-password-page h1, -.check-email-page h1, -.password-confirmed-page h1, -.set-new-password-page h1 { - font-size: 22px; +.auth .field { + position: relative; +} +.auth .input-error { + position: absolute; + top: calc(100% - 2px); } @media (max-width: 800px) { diff --git a/Client/src/Pages/Incidents/IncidentTable/index.jsx b/Client/src/Pages/Incidents/IncidentTable/index.jsx index 7a77208f8..aa80b35ce 100644 --- a/Client/src/Pages/Incidents/IncidentTable/index.jsx +++ b/Client/src/Pages/Incidents/IncidentTable/index.jsx @@ -21,14 +21,16 @@ import { useSelector } from "react-redux"; import { networkService } from "../../../main"; import { StatusLabel } from "../../../Components/Label"; import { logger } from "../../../Utils/Logger"; +import { useTheme } from "@emotion/react"; const IncidentTable = ({ monitors, selectedMonitor, filter }) => { + const theme = useTheme(); const { authToken, user } = useSelector((state) => state.auth); const [checks, setChecks] = useState([]); const [checksCount, setChecksCount] = useState(0); const [paginationController, setPaginationController] = useState({ page: 0, - rowsPerPage: 12, + rowsPerPage: 14, }); useEffect(() => { @@ -46,9 +48,9 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { try { let res; if (selectedMonitor === "0") { - res = await networkService.getChecksByUser( + res = await networkService.getChecksByTeam( authToken, - user._id, + user.teamId, "desc", null, null, @@ -107,26 +109,31 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { next: ArrowForwardRoundedIcon, }} {...item} - sx={{ - "&:focus": { - outline: "none", - }, - }} /> )} /> ); } + let sharedStyles = { + border: 1, + borderColor: theme.palette.border.light, + borderRadius: theme.shape.borderRadius, + backgroundColor: theme.palette.background.main, + p: theme.spacing(30), + }; + return ( <> {checks?.length === 0 && selectedMonitor === "0" ? ( - - No incidents recorded yet. + + + No incidents recorded yet. + ) : checks?.length === 0 ? ( - - + + The monitor you have selected has no recorded incidents yet. @@ -139,6 +146,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { Monitor Name Status Date & Time + Status Code Message
@@ -159,7 +167,10 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { {new Date(check.createdAt).toLocaleString()} - {check.statusCode} + + {check.statusCode ? check.statusCode : "N/A"} + + {check.message} ); })} diff --git a/Client/src/Pages/Incidents/index.css b/Client/src/Pages/Incidents/index.css index 286552d22..73c7b93e0 100644 --- a/Client/src/Pages/Incidents/index.css +++ b/Client/src/Pages/Incidents/index.css @@ -1,7 +1,3 @@ -.incidents h1.MuiTypography-root, -.incidents p.MuiTypography-root { - color: var(--env-var-color-5); -} .incidents h1.MuiTypography-root { font-size: var(--env-var-font-size-large); font-weight: 600; @@ -12,19 +8,4 @@ } .incidents button.MuiButtonBase-root { height: 34px; - border-color: var(--env-var-color-16); -} -.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 { - padding: 0 10px; } diff --git a/Client/src/Pages/Incidents/index.jsx b/Client/src/Pages/Incidents/index.jsx index 34768f7f9..5d756298e 100644 --- a/Client/src/Pages/Incidents/index.jsx +++ b/Client/src/Pages/Incidents/index.jsx @@ -1,7 +1,12 @@ import { useState, useEffect } from "react"; import { useSelector } from "react-redux"; -import { ButtonGroup, Stack, Skeleton, Typography } from "@mui/material"; -import Button from "../../Components/Button"; +import { + ButtonGroup, + Stack, + Skeleton, + Typography, + Button, +} from "@mui/material"; import { networkService } from "../../main"; import { useTheme } from "@emotion/react"; import Select from "../../Components/Inputs/Select"; @@ -18,7 +23,7 @@ const SkeletonLayout = () => { return ( <> - + { useEffect(() => { const fetchMonitors = async () => { setLoading(true); - const res = await networkService.getMonitorsByUserId( + const res = await networkService.getMonitorsByTeamId( authState.authToken, - authState.user._id, + authState.user.teamId, 1, null, null, null, null ); - // Reduce to a lookup object for 0(1) lookup if (res.data && res.data.data.length > 0) { const monitorLookup = res.data.data.reduce((acc, monitor) => { @@ -79,13 +83,17 @@ const Incidents = () => { }; return ( - + {loading ? ( ) : ( <> - - + + Incidents for { items={frequencies} /> - + - + + Delete + diff --git a/Client/src/Pages/Monitors/Configure/skeleton.jsx b/Client/src/Pages/Monitors/Configure/skeleton.jsx new file mode 100644 index 000000000..08eb071cf --- /dev/null +++ b/Client/src/Pages/Monitors/Configure/skeleton.jsx @@ -0,0 +1,52 @@ +import { Box, Skeleton, Stack } from "@mui/material"; +import { useTheme } from "@emotion/react"; + +/** + * Renders a skeleton layout. + * + * @returns {JSX.Element} + */ +const SkeletonLayout = () => { + const theme = useTheme(); + + return ( + <> + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Pages/Monitors/CreateMonitor/index.css b/Client/src/Pages/Monitors/CreateMonitor/index.css index 2a1b5a2a6..ca74e83e1 100644 --- a/Client/src/Pages/Monitors/CreateMonitor/index.css +++ b/Client/src/Pages/Monitors/CreateMonitor/index.css @@ -1,23 +1,11 @@ .create-monitor h1.MuiTypography-root { font-size: var(--env-var-font-size-large-plus); - color: var(--env-var-color-1); font-weight: 500; } -.create-monitor h2.MuiTypography-root { - font-size: var(--env-var-font-size-large); - font-weight: 600; -} .create-monitor p.MuiTypography-root, .create-monitor button.MuiButtonBase-root { font-size: var(--env-var-font-size-medium); } -.create-monitor h2.MuiTypography-root { - color: var(--env-var-color-5); -} -.create-monitor h6.MuiTypography-root, -.create-monitor p.MuiTypography-root { - color: var(--env-var-color-2); -} .create-monitor h6.MuiTypography-root, .create-monitor .MuiBox-root .field + p.MuiTypography-root { font-size: var(--env-var-font-size-small-plus); @@ -26,11 +14,11 @@ .create-monitor button.MuiButtonBase-root { height: 34px; } + .create-monitor .error-container { position: relative; } .create-monitor .error-container p.MuiTypography-root.input-error { - color: var(--env-var-color-24); opacity: 0.8; position: absolute; top: 0; @@ -42,30 +30,9 @@ position: absolute; top: 100%; } -.create-monitor .config-box { - flex-direction: row; - border: 1px solid var(--env-var-color-16); - border-radius: var(--env-var-radius-1); - background-color: var(--env-var-color-8); -} -.create-monitor .config-box > .MuiBox-root, -.create-monitor .config-box > .MuiStack-root { - flex: 1; - padding: 24px 30px; - padding-bottom: 40px; -} -.create-monitor .config-box > *:first-child { - flex: 0.7; - border-right: solid 1px var(--env-var-color-16); -} .create-monitor .MuiStack-root .MuiButtonGroup-root button { font-size: var(--env-var-font-size-small); height: 28px; padding: 8px; } - -body:has(.create-monitor) .select-dropdown .MuiMenuItem-root, -.create-monitor .select-wrapper .select-component > .MuiSelect-select { - text-transform: lowercase; -} diff --git a/Client/src/Pages/Monitors/CreateMonitor/index.jsx b/Client/src/Pages/Monitors/CreateMonitor/index.jsx index 05e343e77..5147236e2 100644 --- a/Client/src/Pages/Monitors/CreateMonitor/index.jsx +++ b/Client/src/Pages/Monitors/CreateMonitor/index.jsx @@ -1,19 +1,19 @@ -import "./index.css"; import { useState } from "react"; -import RadioButton from "../../../Components/RadioButton"; -import Button from "../../../Components/Button"; -import { Box, ButtonGroup, Stack, Typography } from "@mui/material"; +import { Box, Button, ButtonGroup, Stack, Typography } from "@mui/material"; import { useSelector, useDispatch } from "react-redux"; import { monitorValidation } from "../../../Validation/validation"; import { createUptimeMonitor } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; import { useNavigate } from "react-router-dom"; import { useTheme } from "@emotion/react"; +import { createToast } from "../../../Utils/toastUtils"; +import { logger } from "../../../Utils/Logger"; +import { ConfigBox } from "../styled"; +import Radio from "../../../Components/Inputs/Radio"; import Field from "../../../Components/Inputs/Field"; import Select from "../../../Components/Inputs/Select"; import Checkbox from "../../../Components/Inputs/Checkbox"; -import { createToast } from "../../../Utils/toastUtils"; import Breadcrumbs from "../../../Components/Breadcrumbs"; -import { logger } from "../../../Utils/Logger"; +import "./index.css"; const CreateMonitor = () => { const MS_PER_MINUTE = 60000; @@ -33,7 +33,7 @@ const CreateMonitor = () => { const [monitor, setMonitor] = useState({ url: "", name: "", - type: "", + type: "http", notifications: [], interval: 1, }); @@ -118,6 +118,7 @@ const CreateMonitor = () => { form = { ...form, description: form.name, + teamId: user.teamId, userId: user._id, notifications: monitor.notifications, }; @@ -143,7 +144,14 @@ const CreateMonitor = () => { ]; return ( - + { onSubmit={handleCreateMonitor} noValidate spellCheck="false" - gap={theme.gap.large} - mt={theme.gap.medium} + gap={theme.spacing(12)} + mt={theme.spacing(6)} > Create your{" "} @@ -171,15 +179,15 @@ const CreateMonitor = () => { monitor - + General settings - + Here you can select the URL of the host, together with the type of monitor. - + { { error={errors["name"]} /> - - + + Checks to perform - + You can always add or remove checks after adding your site. - - - + + { {monitor.type === "http" ? ( ) : ( "" )} - { /> {errors["type"] ? ( - + {errors["type"]} @@ -264,15 +272,15 @@ const CreateMonitor = () => { "" )} - - + + Incident notifications - + When there is an incident, notify users. - + When there is a new incident, { {monitor.notifications.some( (notification) => notification.type === "emails" ) ? ( - + { value="" onChange={() => logger.warn("disabled")} /> - + You can separate multiple emails with a comma @@ -318,12 +326,12 @@ const CreateMonitor = () => { "" )} - - + + Advanced settings - + { items={[{ _id: "est", name: "America / Toronto" }]} /> - - - - History and monitoring - - Define here for how long you want to keep the data. You can also - remove all past data. - - - - logger.warn("Disabled")} - /> + + {isAdmin && ( + - Clear all stats. This is irreversible. - - + ); }; +Settings.propTypes = { + isAdmin: PropTypes.bool, +}; export default Settings; diff --git a/Client/src/Pages/Status/index.jsx b/Client/src/Pages/Status/index.jsx index d280132c4..cb759b33b 100644 --- a/Client/src/Pages/Status/index.jsx +++ b/Client/src/Pages/Status/index.jsx @@ -1,8 +1,25 @@ +import { Box } from "@mui/material"; +import { useTheme } from "@emotion/react"; import Fallback from "../../Components/Fallback"; const Status = () => { + const theme = useTheme(); + return ( -
+ [class*="fallback__"])': { + position: "relative", + border: 1, + borderColor: theme.palette.border.light, + borderRadius: theme.shape.borderRadius, + borderStyle: "dashed", + backgroundColor: theme.palette.background.main, + overflow: "hidden", + }, + }} + > { "Build trust with your customers", ]} /> -
+ ); }; diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js index de4508cff..31a23b5b1 100644 --- a/Client/src/Utils/NetworkService.js +++ b/Client/src/Utils/NetworkService.js @@ -17,6 +17,27 @@ class NetworkService { ); } + /** + * + * ************************************ + * Create a new monitor + * ************************************ + * + * @async + * @param {string} authToken - The authorization token to be used in the request header. + * @param {string} monitorId - The monitor ID to be sent in the param. + * @returns {Promise} The response from the axios GET request. + */ + + async getMonitorByid(authToken, monitorId) { + return this.axiosInstance.get(`/monitors/${monitorId}`, { + headers: { + Authorization: `Bearer ${authToken}`, + "Content-Type": "application/json", + }, + }); + } + /** * * ************************************ @@ -52,9 +73,9 @@ class NetworkService { * @param {boolean} [normalize] - Whether to normalize the retrieved monitors. * @returns {Promise} The response from the axios GET request. */ - async getMonitorsByUserId( + async getMonitorsByTeamId( authToken, - userId, + teamId, limit, types, status, @@ -74,7 +95,7 @@ class NetworkService { if (normalize) params.append("normalize", normalize); return this.axiosInstance.get( - `/monitors/user/${userId}?${params.toString()}`, + `/monitors/team/${teamId}?${params.toString()}`, { headers: { Authorization: `Bearer ${authToken}`, @@ -164,6 +185,47 @@ class NetworkService { }); } + /** + * ************************************ + * Deletes all checks for all monitor by teamID + * ************************************ + * + * @async + * @param {string} authToken - The authorization token to be used in the request header. + * @param {string} monitorId - The ID of the monitor to be deleted. + * @returns {Promise} The response from the axios DELETE request. + */ + async deleteChecksByTeamId(authToken, teamId) { + return this.axiosInstance.delete(`/checks/team/${teamId}`, { + headers: { + Authorization: `Bearer ${authToken}`, + "Content-Type": "application/json", + }, + }); + } + /** + * ************************************ + * Pauses a single monitor by its ID + * ************************************ + * + * @async + * @param {string} authToken - The authorization token to be used in the request header. + * @param {string} monitorId - The ID of the monitor to be paused. + * @returns {Promise} The response from the axios POST request. + */ + async pauseMonitorById(authToken, monitorId) { + return this.axiosInstance.post( + `/monitors/pause/${monitorId}`, + {}, + { + headers: { + Authorization: `Bearer ${authToken}`, + "Content-Type": "application/json", + }, + } + ); + } + /** * ************************************ * Gets the certificate expiry for a monitor @@ -226,7 +288,6 @@ class NetworkService { return this.axiosInstance.put(`/auth/user/${userId}`, form, { headers: { Authorization: `Bearer ${authToken}`, - "Content-Type": "application/json", }, }); } @@ -293,8 +354,8 @@ class NetworkService { * @returns {Promise} The response from the axios GET request. * */ - async doesAdminExist() { - return this.axiosInstance.get("/auth/users/admin"); + async doesSuperAdminExist() { + return this.axiosInstance.get("/auth/users/superadmin"); } /** @@ -327,7 +388,7 @@ class NetworkService { */ async requestInvitationToken(authToken, email, role) { return this.axiosInstance.post( - `/auth/invite`, + `/invite`, { email, role }, { headers: { Authorization: `Bearer ${authToken}` }, @@ -346,7 +407,7 @@ class NetworkService { * */ async verifyInvitationToken(token) { - return this.axiosInstance.post(`/auth/invite/verify`, { + return this.axiosInstance.post(`/invite/verify`, { token, }); } @@ -409,9 +470,9 @@ class NetworkService { * @returns {Promise} The response from the axios GET request. * */ - async getChecksByUser( + async getChecksByTeam( authToken, - userId, + teamId, sortOrder, limit, dateRange, @@ -427,7 +488,7 @@ class NetworkService { if (page) params.append("page", page); if (rowsPerPage) params.append("rowsPerPage", rowsPerPage); return this.axiosInstance.get( - `/checks/user/${userId}?${params.toString()}`, + `/checks/team/${teamId}?${params.toString()}`, { headers: { Authorization: `Bearer ${authToken}` }, } diff --git a/Client/src/Utils/Theme.js b/Client/src/Utils/Theme.js deleted file mode 100644 index c4faa2d1f..000000000 --- a/Client/src/Utils/Theme.js +++ /dev/null @@ -1,171 +0,0 @@ -import { createTheme } from "@mui/material"; - -// Colors for MUI theme -const primaryMain = "#1570EF"; -const secondaryMain = "#475467"; -const tertiaryMain = "#475467"; -const tertiaryLinkHover = "#eff8ff"; - -// Label Colors -const labelOrange = "#F79009"; -const labelGray = "#475467"; -const labelPurple = "#6941C6"; -const labelGreen = "#067647"; -const labelRed = "#F04438"; - -// Section colors -const sectionBorder = "#D0D5DD"; -const sectionBg = "#F8F9F8"; - -// Other Colors -const otherColorsBlackish = "#101828"; -const otherColorsBluishGray = "#344054"; -const otherColorsGraishWhite = "#eaecf0"; -const otherColorsPurple = "#7f56d9"; -const otherColorsWhite = "#fff"; -const otherColorsGraishWhiteLight = "#f2f2f2"; -const otherColorsStrongBlue = "#f2f2f2"; -const otherColorsSlateGray = "#667085"; -const otherColorsFillGray = "#f4f4f4"; - -//box shadow -const shadow = - "0px 4px 24px -4px rgba(16, 24, 40, 0.08), 0px 3px 3px -3px rgba(16, 24, 40, 0.03)"; - -// Global Font Family -const fontFamilyDefault = - '"Inter","system-ui", "Avenir", "Helvetica", "Arial", sans-serif'; - -const theme = createTheme({ - typography: { fontFamily: fontFamilyDefault, fontSize: 13 }, - palette: { - primary: { - main: primaryMain, - }, - secondary: { - main: secondaryMain, - }, - tertiary: { - main: tertiaryMain, - linkHover: tertiaryLinkHover, - }, - labelOrange: { - color: labelOrange, - }, - labelGray: { - color: labelGray, - }, - labelPurple: { - color: labelPurple, - }, - labelGreen: { - color: labelGreen, - }, - labelRed: { - color: labelRed, - }, - section: { - borderColor: sectionBorder, - bgColor: sectionBg, - }, - otherColors: { - blackish: otherColorsBlackish, - bluishGray: otherColorsBluishGray, - graishWhite: otherColorsGraishWhite, - purple: otherColorsPurple, - white: otherColorsWhite, - graishWhiteLight: otherColorsGraishWhiteLight, - strongBlue: otherColorsStrongBlue, - slateGray: otherColorsSlateGray, - fillGray: otherColorsFillGray, - }, - }, - shape: { - borderRadius: 4, - borderThick: 2, - boxShadow: shadow, - }, - content: { - pX: "80px", - pY: "40px", - }, - gap: { - xs: "4px", - small: "8px", - medium: "12px", - ml: "16px", - mlplus: "20px", - large: "24px", - lgplus: "32px", - xl: "40px", - xxl: "60px", - triplexl: "120px", - }, - alert: { - info: { - color: secondaryMain, - bg: otherColorsWhite, - border: sectionBorder, - }, - error: { - color: "#d92d20", - bg: "hsla(0, 100%, 52%, 0.03)", - border: "#f04438", - }, - warning: { - color: "#DC6803", - bg: "#fffcf5", - border: "#fec84b", - }, - }, - //add more as needed - label: { - up: { - borderColor: "#D4F4E1", - bgColor: "#ecfdf3", - dotColor: "#17B26A", - }, - down: { - borderColor: "#fbd1d1", - bgColor: "#f9eced", - dotColor: "#f04438", - }, - "cannot resolve": { - borderColor: "#e2eaf7", - bgColor: "#f2f4f7", - dotColor: "#4e5ba6", - }, - }, - pie: { - green: { - stroke: "#17b26a", - strokeBg: "#d4f4e1", - text: "#079455", - bg: "#ecfdf3", - shape: "circle", - }, - yellow: { - stroke: "#fdb022", - strokeBg: "rgba(255, 192, 34, 0.3)", - text: "#dc6803", - bg: "#fffcf5", - shape: "square", - }, - red: { - stroke: "#f04438", - strokeBg: "#ffecea", - text: "#f04438", - bg: "#ffeaea", - shape: "triangle", - }, - default: { - stroke: "#4e5ba6", - strokeBg: "#f2f4f7", - text: "#4e5ba6", - bg: "#f2f4f7", - shape: "", - }, - }, -}); - -export default theme; diff --git a/Client/src/Utils/Theme/darkTheme.js b/Client/src/Utils/Theme/darkTheme.js new file mode 100644 index 000000000..15df7c1b8 --- /dev/null +++ b/Client/src/Utils/Theme/darkTheme.js @@ -0,0 +1,216 @@ +import { createTheme } from "@mui/material"; + +const text = { + primary: "#fafafa", + secondary: "#e6e6e6", + tertiary: "#a1a1aa", + accent: "#e6e6e6", + disabled: "rgba(172, 172, 172, 0.3)", +}; +const background = { + main: "#151518", + alt: "#09090b", + fill: "#2e2e2e", + accent: "#18181a", +}; +const border = { light: "#27272a", dark: "#2c2c2c" }; + +const fontFamilyDefault = + '"Inter","system-ui", "Avenir", "Helvetica", "Arial", sans-serif'; +const shadow = + "0px 4px 24px -4px rgba(16, 24, 40, 0.08), 0px 3px 3px -3px rgba(16, 24, 40, 0.03)"; + +const darkTheme = createTheme({ + typography: { fontFamily: fontFamilyDefault, fontSize: 13 }, + palette: { + mode: "dark", + primary: { main: "#1570ef" }, + secondary: { main: "#2e2e2e" }, + text: text, + background: background, + border: border, + info: { + text: text.primary, + main: text.secondary, + bg: background.main, + light: background.main, + border: border.light, + }, + success: { + text: "#079455", + main: "#45bb7a", + light: "#1e1e1e", + bg: "#27272a", + }, + error: { + text: "#f04438", + main: "#d32f2f", + light: "#1e1e1e", + bg: "#27272a", + dark: "#932020", + border: "#f04438", + }, + warning: { + text: "#e88c30", + main: "#FF9F00", + light: "#27272a", + bg: "#1E1E1E", + border: "#e88c30", + }, + unresolved: { main: "#4e5ba6", light: "#e2eaf7", bg: "#f2f4f7" }, + divider: border.light, + other: { + icon: "#e6e6e6", + line: "#27272a", + fill: "#18181a", + grid: "#454546", + }, + }, + spacing: 2, + components: { + MuiButton: { + defaultProps: { + disableRipple: true, + }, + styleOverrides: { + root: ({ theme }) => ({ + variants: [ + { + props: (props) => props.variant === "group", + style: { + color: theme.palette.secondary.contrastText, + backgroundColor: theme.palette.background.main, + border: 1, + borderStyle: "solid", + borderColor: theme.palette.border.light, + }, + }, + { + props: (props) => + props.variant === "group" && props.filled === "true", + style: { + backgroundColor: theme.palette.secondary.main, + }, + }, + ], + fontWeight: 400, + borderRadius: 4, + boxShadow: "none", + textTransform: "none", + "&:focus": { + outline: "none", + }, + "&:hover": { + boxShadow: "none", + }, + }), + }, + }, + MuiIconButton: { + styleOverrides: { + root: { + padding: 4, + transition: "none", + "&:hover": { + backgroundColor: background.fill, + }, + }, + }, + }, + MuiPaper: { + styleOverrides: { + root: { + marginTop: 4, + padding: 0, + border: 1, + borderStyle: "solid", + borderColor: border.light, + borderRadius: 4, + boxShadow: shadow, + backgroundColor: background.main, + backgroundImage: "none", + }, + }, + }, + MuiList: { + styleOverrides: { + root: { + padding: 0, + }, + }, + }, + MuiListItemButton: { + styleOverrides: { + root: { + transition: "none", + }, + }, + }, + MuiMenuItem: { + styleOverrides: { + root: { + borderRadius: 4, + backgroundColor: "inherit", + padding: "4px 6px", + color: text.secondary, + fontSize: 13, + margin: 2, + minWidth: 100, + "&:hover, &.Mui-selected, &.Mui-selected:hover, &.Mui-selected.Mui-focusVisible": + { + backgroundColor: background.fill, + }, + }, + }, + }, + MuiTableCell: { + styleOverrides: { + root: { + borderBottomColor: border.light, + }, + }, + }, + MuiTableHead: { + styleOverrides: { + root: { + backgroundColor: background.accent, + }, + }, + }, + MuiPagination: { + styleOverrides: { + root: { + backgroundColor: background.main, + border: 1, + borderStyle: "solid", + borderColor: border.light, + "& button": { + color: text.tertiary, + borderRadius: 4, + }, + "& li:first-of-type button, & li:last-of-type button": { + border: 1, + borderStyle: "solid", + borderColor: border.light, + }, + }, + }, + }, + MuiPaginationItem: { + styleOverrides: { + root: { + "&:not(.MuiPaginationItem-ellipsis):hover, &.Mui-selected": { + backgroundColor: background.fill, + }, + }, + }, + }, + }, + shape: { + borderRadius: 2, + borderThick: 2, + boxShadow: shadow, + }, +}); + +export default darkTheme; diff --git a/Client/src/Utils/Theme/lightTheme.js b/Client/src/Utils/Theme/lightTheme.js new file mode 100644 index 000000000..bb83d09b2 --- /dev/null +++ b/Client/src/Utils/Theme/lightTheme.js @@ -0,0 +1,212 @@ +import { createTheme } from "@mui/material"; + +const text = { + primary: "#1c2130", + secondary: "#344054", + tertiary: "#475467", + accent: "#838c99", +}; +const background = { + main: "#FFFFFF", + alt: "#FCFCFD", + fill: "#F4F4F4", + accent: "#f9fafb", +}; +const border = { light: "#eaecf0", dark: "#d0d5dd" }; + +const fontFamilyDefault = + '"Inter","system-ui", "Avenir", "Helvetica", "Arial", sans-serif'; +const shadow = + "0px 4px 24px -4px rgba(16, 24, 40, 0.08), 0px 3px 3px -3px rgba(16, 24, 40, 0.03)"; + +const lightTheme = createTheme({ + typography: { fontFamily: fontFamilyDefault, fontSize: 13 }, + palette: { + primary: { main: "#1570EF" }, + secondary: { main: "#F4F4F4", dark: "#e3e3e3", contrastText: "#475467" }, + text: text, + background: background, + border: border, + info: { + text: text.primary, + main: text.tertiary, + bg: background.main, + light: background.main, + border: border.dark, + }, + success: { + text: "#079455", + main: "#17b26a", + light: "#d4f4e1", + bg: "#ecfdf3", + }, + error: { + text: "#f04438", + main: "#d32f2f", + light: "#fbd1d1", + bg: "#f9eced", + border: "#f04438", + }, + warning: { + text: "#DC6803", + main: "#fdb022", + light: "#fffcf5", + bg: "#ffecbc", + border: "#fec84b", + }, + unresolved: { main: "#4e5ba6", light: "#e2eaf7", bg: "#f2f4f7" }, + divider: border.light, + other: { + icon: "#667085", + line: "#d6d9dd", + fill: "#e3e3e3", + grid: "#a2a3a3", + }, + }, + spacing: 2, + components: { + MuiButton: { + defaultProps: { + disableRipple: true, + }, + styleOverrides: { + root: ({ theme }) => ({ + variants: [ + { + props: (props) => props.variant === "group", + style: { + color: theme.palette.secondary.contrastText, + backgroundColor: theme.palette.background.main, + border: 1, + borderStyle: "solid", + borderColor: theme.palette.border.light, + }, + }, + { + props: (props) => + props.variant === "group" && props.filled === "true", + style: { + backgroundColor: theme.palette.secondary.main, + }, + }, + ], + fontWeight: 400, + borderRadius: 4, + boxShadow: "none", + textTransform: "none", + "&:focus": { + outline: "none", + }, + "&:hover": { + boxShadow: "none", + }, + }), + }, + }, + MuiIconButton: { + styleOverrides: { + root: { + padding: 4, + transition: "none", + "&:hover": { + backgroundColor: background.fill, + }, + }, + }, + }, + MuiPaper: { + styleOverrides: { + root: { + marginTop: 4, + border: 1, + borderStyle: "solid", + borderColor: border.light, + borderRadius: 4, + boxShadow: shadow, + backgroundColor: background.main, + }, + }, + }, + MuiList: { + styleOverrides: { + root: { + padding: 0, + }, + }, + }, + MuiListItemButton: { + styleOverrides: { + root: { + transition: "none", + }, + }, + }, + MuiMenuItem: { + styleOverrides: { + root: { + borderRadius: 4, + backgroundColor: "inherit", + padding: "4px 6px", + color: text.secondary, + fontSize: 13, + margin: 2, + marginBottom: 0, + minWidth: 100, + "&:hover, &.Mui-selected, &.Mui-selected:hover, &.Mui-selected.Mui-focusVisible": + { + backgroundColor: background.fill, + }, + }, + }, + }, + MuiTableCell: { + styleOverrides: { + root: { + borderBottomColor: border.light, + }, + }, + }, + MuiTableHead: { + styleOverrides: { + root: { + backgroundColor: background.accent, + }, + }, + }, + MuiPagination: { + styleOverrides: { + root: { + backgroundColor: background.main, + border: 1, + borderStyle: "solid", + borderColor: border.light, + "& button": { + color: text.tertiary, + borderRadius: 4, + }, + "& li:first-of-type button, & li:last-of-type button": { + border: 1, + borderStyle: "solid", + borderColor: border.light, + }, + }, + }, + }, + MuiPaginationItem: { + styleOverrides: { + root: { + "&:not(.MuiPaginationItem-ellipsis):hover, &.Mui-selected": { + backgroundColor: background.fill, + }, + }, + }, + }, + }, + shape: { + borderRadius: 2, + borderThick: 2, + boxShadow: shadow, + }, +}); + +export default lightTheme; diff --git a/Client/src/Utils/greeting.jsx b/Client/src/Utils/greeting.jsx new file mode 100644 index 000000000..cb878c991 --- /dev/null +++ b/Client/src/Utils/greeting.jsx @@ -0,0 +1,192 @@ +import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; +import { Box, Typography } from "@mui/material"; +import { useDispatch, useSelector } from "react-redux"; +import { useEffect } from "react"; +import { setGreeting } from "../Features/UI/uiSlice"; + +const early = [ + { + prepend: "Rise and shine", + append: "If you’re up this early, you might as well be a legend!", + emoji: "☕", + }, + { + prepend: "Good morning", + append: "The world’s still asleep, but you’re already awesome!", + emoji: "🦉", + }, + { + prepend: "Good morning", + append: "Are you a wizard? Only magical people are up at this hour!", + emoji: "🌄", + }, + { + prepend: "Up before the roosters", + append: "Ready to tackle the day before it even starts?", + emoji: "🐓", + }, + { + prepend: "Early bird special", + append: "Let’s get things done while everyone else is snoozing!", + emoji: "🌟", + }, +]; + +const morning = [ + { + prepend: "Good morning", + append: "Is it coffee o’clock yet, or should we start with high fives?", + emoji: "☕", + }, + { + prepend: "Morning", + append: "The sun is up, and so are you—time to be amazing!", + emoji: "🌞", + }, + { + prepend: "Good morning", + append: "Time to make today the best thing since sliced bread!", + emoji: "🥐", + }, + { + prepend: "Morning", + append: "Let’s kick off the day with more energy than a double espresso!", + emoji: "🚀", + }, + { + prepend: "Rise and shine", + append: "You’re about to make today so great, even Monday will be jealous!", + emoji: "🌟", + }, +]; + +const afternoon = [ + { + prepend: "Good afternoon", + append: "How about a break to celebrate how awesome you’re doing?", + emoji: "🥪", + }, + { + prepend: "Afternoon", + append: "If you’re still going strong, you’re officially a rockstar!", + emoji: "🌞", + }, + { + prepend: "Hey there", + append: "The afternoon is your playground—let’s make it epic!", + emoji: "🍕", + }, + { + prepend: "Good afternoon", + append: "Time to crush the rest of the day like a pro!", + emoji: "🏆", + }, + { + prepend: "Afternoon", + append: "Time to turn those afternoon slumps into afternoon triumphs!", + emoji: "🎉", + }, +]; + +const evening = [ + { + prepend: "Good evening", + append: "Time to wind down and think about how you crushed today!", + emoji: "🌇", + }, + { + prepend: "Evening", + append: "You’ve earned a break—let’s make the most of these evening vibes!", + emoji: "🍹", + }, + { + prepend: "Hey there", + append: "Time to relax and bask in the glow of your day’s awesomeness!", + emoji: "🌙", + }, + { + prepend: "Good evening", + append: "Ready to trade productivity for chill mode?", + emoji: "🛋️ ", + }, + { + prepend: "Evening", + append: "Let’s call it a day and toast to your success!", + emoji: "🕶️", + }, +]; + +/** + * Greeting component that displays a personalized greeting message + * based on the time of day and the user's first name. + * + * @component + * @example + * return ; + * + * @param {Object} props + * @param {string} props.type - The type of monitor to be displayed in the message + * @returns {JSX.Element} The rendered Greeting component + */ + +const Greeting = ({ type = "" }) => { + const theme = useTheme(); + const dispatch = useDispatch(); + const { firstName } = useSelector((state) => state.auth.user); + const index = useSelector((state) => state.ui.greeting.index); + const lastUpdate = useSelector((state) => state.ui.greeting.lastUpdate); + + const now = new Date(); + const hour = now.getHours(); + + useEffect(() => { + const hourDiff = lastUpdate ? hour - lastUpdate : null; + + if (!lastUpdate || hourDiff >= 1) { + let random = Math.floor(Math.random() * 5); + dispatch(setGreeting({ index: random, lastUpdate: hour })); + } + }, [dispatch, hour]); + + let greetingArray = + hour < 6 ? early : hour < 12 ? morning : hour < 18 ? afternoon : evening; + const { prepend, append, emoji } = greetingArray[index]; + + return ( + + + + {prepend},{" "} + + + {firstName} {emoji} + + + + {append} — Here’s an overview of your {type} monitors. + + + ); +}; + +Greeting.propTypes = { + type: PropTypes.string, +}; + +export default Greeting; diff --git a/Client/src/Utils/toastUtils.jsx b/Client/src/Utils/toastUtils.jsx index ff3f341f8..0383ba466 100644 --- a/Client/src/Utils/toastUtils.jsx +++ b/Client/src/Utils/toastUtils.jsx @@ -19,7 +19,7 @@ export const createToast = ({ config = {}, }) => { const toastConfig = { - position: "bottom-right", + position: "top-right", autoClose: 3000, hideProgressBar: true, closeButton: false, diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js index 6aa7107b6..948c228cc 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -76,6 +76,7 @@ const credentials = joi.object({ return value; }), role: joi.array(), + teamId: joi.string().allow("").optional(), }); const monitorValidation = joi.object({ @@ -135,9 +136,9 @@ const maintenanceWindowValidation = joi.object({ "any.only": "Unit must be one of ['minutes', 'hours', 'days'].", "any.required": "Unit is required.", }), - friendlyName: joi.string().max(50).required().messages({ - "string.empty": "Friendly name is required.", - "string.max": "Friendly name must be less than 50 characters long", + displayName: joi.string().max(50).required().messages({ + "string.empty": "Display name is required.", + "string.max": "Display name must be less than 50 characters long", }), addMonitors: joi.string().max(50).required().messages({ "string.empty": "Add monitors is required.", diff --git a/Client/src/assets/Images/create-placeholder-dark.svg b/Client/src/assets/Images/create-placeholder-dark.svg new file mode 100644 index 000000000..2dd68750b --- /dev/null +++ b/Client/src/assets/Images/create-placeholder-dark.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Client/src/assets/icons/dots-vertical.svg b/Client/src/assets/icons/dots-vertical.svg new file mode 100644 index 000000000..e720066c4 --- /dev/null +++ b/Client/src/assets/icons/dots-vertical.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Client/src/assets/icons/radio-checked.svg b/Client/src/assets/icons/radio-checked.svg new file mode 100644 index 000000000..1c584b79a --- /dev/null +++ b/Client/src/assets/icons/radio-checked.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Client/src/index.css b/Client/src/index.css index d513ac9a1..105776bd8 100644 --- a/Client/src/index.css +++ b/Client/src/index.css @@ -7,59 +7,12 @@ :root { font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; font-weight: 400; - - /* color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; */ - font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; /* Generalized Stylings */ - - --env-var-font-familt-1: "Roboto", "Helvetica", "Arial", sans-serif; - - --env-var-color-0: #fff; - --env-var-color-1: #1c2130; - --env-var-color-2: #475467; - --env-var-color-2-light: #838c99; - --env-var-color-3: #1570ef; - - --env-var-color-4: #d0d5dd; - --env-var-color-5: #344054; - --env-var-color-6: #eaecf0; - --env-var-color-7: #7f56d9; - --env-var-color-8: #fff; - --env-var-color-9: #f2f2f2; - --env-var-color-10: #175cd3; - --env-var-color-11: #5d6b98; - --env-var-color-12: #182230; - --env-var-color-13: #f9fafb; - --env-var-color-14: #98a2b3; - --env-var-color-15: #f2f4f7; - --env-var-color-16: #ebebeb; - --env-var-color-17: #079455; - --env-var-color-18: #ffc301; - --env-var-color-19: #f04438; - --env-var-color-20: #ecfdf3; - --env-var-color-21: hsla(0, 100%, 52%, 0.06); - --env-var-color-22: #4e5ba6; - --env-var-color-23: #17b26a; - --env-var-color-24: #d92d20; - --env-var-color-25: #667085; - --env-var-color-26: #ff1818; - - --env-var-color-27: #fecf60; - --lighter-env-var-color-27: rgba(254, 207, 96, 0.1); - --env-var-color-28: #f79009; - --env-var-color-29: #d0d5dd; - --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; @@ -99,48 +52,12 @@ 0px 3px 3px -3px rgba(16, 24, 40, 0.03); } -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} - -a:hover { - color: #535bf2; -} - body { margin: 0; min-width: 320px; height: 100%; } -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} - -button:hover { - border-color: #646cff; -} - -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - .MuiInputBase-root.Mui-disabled input { cursor: not-allowed; } @@ -150,6 +67,8 @@ button:focus-visible { } .Toastify__toast-body .alert { min-width: 150px; + padding: 5px 10px; + align-items: center; } .Toastify [class^="Toastify__toast"] { padding: 0; @@ -159,13 +78,16 @@ button:focus-visible { min-height: 0; border-radius: 4px; } +.Toastify [class*="Toastify__toast-theme"] { + background-color: transparent; +} .MuiTouchRipple-root { display: none; } body .MuiSkeleton-root { - background-color: var(--env-var-color-15); + background-color: #f2f4f7; } @keyframes ripple { @@ -178,18 +100,3 @@ body .MuiSkeleton-root { transform: scale(2); } } - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - - a:hover { - color: #747bff; - } - - button { - background-color: #f9f9f9; - } -} diff --git a/Client/src/main.jsx b/Client/src/main.jsx index 00cec7d14..d592f9137 100644 --- a/Client/src/main.jsx +++ b/Client/src/main.jsx @@ -3,8 +3,6 @@ import ReactDOM from "react-dom/client"; import App from "./App.jsx"; import "./index.css"; import { BrowserRouter as Router, HashRouter } from "react-router-dom"; -import theme from "./Utils/Theme.js"; -import { ThemeProvider } from "@mui/material"; import { Provider } from "react-redux"; import { persistor, store } from "./store"; import { PersistGate } from "redux-persist/integration/react"; @@ -13,11 +11,9 @@ export const networkService = new NetworkService(store); ReactDOM.createRoot(document.getElementById("root")).render( - - - - - + + + ); diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index 6fbcdb669..0cf297a47 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -1,6 +1,6 @@ const express = require("express"); const { - registerValidation, + registrationBodyValidation, loginValidation, editUserParamValidation, editUserBodyValidation, @@ -50,7 +50,7 @@ const getTokenFromHeaders = (headers) => { const registerController = async (req, res, next) => { // joi validation try { - await registerValidation.validateAsync(req.body); + await registrationBodyValidation.validateAsync(req.body); } catch (error) { error.status = 422; error.service = SERVICE_NAME; @@ -78,7 +78,7 @@ const registerController = async (req, res, next) => { // Create a new user try { - const newUser = await req.db.insertUser(req, res); + const newUser = await req.db.insertUser({ ...req.body }, req.file); logger.info(successMessages.AUTH_CREATE_USER, { service: SERVICE_NAME, userId: newUser._id, @@ -90,12 +90,20 @@ const registerController = async (req, res, next) => { const token = issueToken(userForToken); - await req.emailService.buildAndSendEmail( - "welcomeEmailTemplate", - { name: newUser.firstName }, - newUser.email, - "Welcome to Uptime Monitor" - ); + req.emailService + .buildAndSendEmail( + "welcomeEmailTemplate", + { name: newUser.firstName }, + newUser.email, + "Welcome to Uptime Monitor" + ) + .catch((error) => { + logger.error("Error sending welcome email", { + service: SERVICE_NAME, + error: error.message, + }); + }); + return res.status(200).json({ success: true, msg: successMessages.AUTH_CREATE_USER, @@ -143,8 +151,7 @@ const loginController = async (req, res, next) => { // Happy path, return token const token = issueToken(userWithoutPassword); - user.authToken = token; - await user.save(); + // reset avatar image userWithoutPassword.avatarImage = user.avatarImage; @@ -226,7 +233,8 @@ const inviteController = async (req, res, next) => { try { // Only admins can invite const token = getTokenFromHeaders(req.headers); - const { role, firstname } = jwt.decode(token); + const { role, firstname, teamId } = jwt.decode(token); + req.body.teamId = teamId; try { await inviteRoleValidation.validateAsync({ roles: role }); await inviteBodyValidation.validateAsync(req.body); @@ -240,15 +248,22 @@ const inviteController = async (req, res, next) => { } const inviteToken = await req.db.requestInviteToken(req, res); - await req.emailService.buildAndSendEmail( - "employeeActivationTemplate", - { - name: firstname, - link: `${process.env.CLIENT_HOST}/register/${inviteToken.token}`, - }, - req.body.email, - "Welcome to Uptime Monitor" - ); + req.emailService + .buildAndSendEmail( + "employeeActivationTemplate", + { + name: firstname, + link: `${process.env.CLIENT_HOST}/register/${inviteToken.token}`, + }, + req.body.email, + "Welcome to Uptime Monitor" + ) + .catch((error) => { + logger.error("Error sending invite email", { + service: SERVICE_NAME, + error: error.message, + }); + }); return res .status(200) @@ -274,7 +289,6 @@ const inviteVerifyController = async (req, res, next) => { try { const invite = await req.db.getInviteToken(req, res); - res .status(200) .json({ status: "success", msg: "Invite verified", data: invite }); @@ -293,13 +307,13 @@ const inviteVerifyController = async (req, res, next) => { * @returns {Promise} */ -const checkAdminController = async (req, res) => { +const checkSuperadminController = async (req, res) => { try { - const adminExists = await req.db.checkAdmin(req, res); + const superAdminExists = await req.db.checkSuperadmin(req, res); return res.status(200).json({ success: true, msg: successMessages.AUTH_ADMIN_EXISTS, - data: adminExists, + data: superAdminExists, }); } catch (error) { error.service = SERVICE_NAME; @@ -508,7 +522,7 @@ module.exports = { userEditController, inviteController, inviteVerifyController, - checkAdminController, + checkSuperadminController, recoveryRequestController, validateRecoveryTokenController, resetPasswordController, diff --git a/Server/controllers/checkController.js b/Server/controllers/checkController.js index 957eeed4e..85bce0b54 100644 --- a/Server/controllers/checkController.js +++ b/Server/controllers/checkController.js @@ -3,9 +3,10 @@ const { createCheckBodyValidation, getChecksParamValidation, getChecksQueryValidation, - getUserChecksParamValidation, - getUserChecksQueryValidation, + getTeamChecksParamValidation, + getTeamChecksQueryValidation, deleteChecksParamValidation, + deleteChecksByTeamIdParamValidation, } = require("../validation/joi"); const { successMessages } = require("../utils/messages"); const SERVICE_NAME = "check"; @@ -62,10 +63,10 @@ const getChecks = async (req, res, next) => { } }; -const getUserChecks = async (req, res, next) => { +const getTeamChecks = async (req, res, next) => { try { - await getUserChecksParamValidation.validateAsync(req.params); - await getUserChecksQueryValidation.validateAsync(req.query); + await getTeamChecksParamValidation.validateAsync(req.params); + await getTeamChecksQueryValidation.validateAsync(req.query); } catch (error) { error.status = 422; error.service = SERVICE_NAME; @@ -75,7 +76,7 @@ const getUserChecks = async (req, res, next) => { return; } try { - const checkData = await req.db.getUserChecks(req); + const checkData = await req.db.getTeamChecks(req); return res.status(200).json({ success: true, msg: successMessages.CHECK_GET, @@ -112,9 +113,34 @@ const deleteChecks = async (req, res, next) => { } }; +const deleteChecksByTeamId = async (req, res, next) => { + try { + await deleteChecksByTeamIdParamValidation.validateAsync(req.params); + } catch (error) { + error.service = SERVICE_NAME; + error.method = "deleteChecksByTeam"; + error.status = 422; + next(error); + } + + try { + const deletedCount = await req.db.deleteChecksByTeamId(req.params.teamId); + return res.status(200).json({ + success: true, + msg: successMessages.CHECK_DELETE, + data: { deletedCount }, + }); + } catch (error) { + error.service = SERVICE_NAME; + error.method = "deleteChecksByTeamId"; + next(error); + } +}; + module.exports = { createCheck, getChecks, - getUserChecks, + getTeamChecks, deleteChecks, + deleteChecksByTeamId, }; diff --git a/Server/controllers/inviteController.js b/Server/controllers/inviteController.js new file mode 100644 index 000000000..e685d0eef --- /dev/null +++ b/Server/controllers/inviteController.js @@ -0,0 +1,96 @@ +const { + inviteRoleValidation, + inviteBodyValidation, + inviteVerifciationBodyValidation, +} = require("../validation/joi"); +const logger = require("../utils/logger"); +require("dotenv").config(); +const { errorMessages, successMessages } = require("../utils/messages"); +var jwt = require("jsonwebtoken"); +const SERVICE_NAME = "inviteController"; + +const getTokenFromHeaders = (headers) => { + const authorizationHeader = headers.authorization; + if (!authorizationHeader) throw new Error("No auth headers"); + + const parts = authorizationHeader.split(" "); + if (parts.length !== 2 || parts[0] !== "Bearer") + throw new Error("Invalid auth headers"); + + return parts[1]; +}; + +const inviteController = async (req, res, next) => { + try { + // Only admins can invite + const token = getTokenFromHeaders(req.headers); + const { role, firstname, teamId } = jwt.decode(token); + req.body.teamId = teamId; + try { + await inviteRoleValidation.validateAsync({ roles: role }); + await inviteBodyValidation.validateAsync(req.body); + } catch (error) { + error.status = 422; + error.service = SERVICE_NAME; + error.message = + error.details?.[0]?.message || error.message || "Validation Error"; + next(error); + return; + } + + const inviteToken = await req.db.requestInviteToken({ ...req.body }); + req.emailService + .buildAndSendEmail( + "employeeActivationTemplate", + { + name: firstname, + link: `${process.env.CLIENT_HOST}/register/${inviteToken.token}`, + }, + req.body.email, + "Welcome to Uptime Monitor" + ) + .catch((error) => { + logger.error("Error sending invite email", { + service: SERVICE_NAME, + error: error.message, + }); + }); + + return res + .status(200) + .json({ success: true, msg: "Invite sent", data: inviteToken }); + } catch (error) { + error.service = SERVICE_NAME; + next(error); + return; + } +}; + +const inviteVerifyController = async (req, res, next) => { + try { + await inviteVerifciationBodyValidation.validateAsync(req.body); + } catch (error) { + error.status = 422; + error.service = SERVICE_NAME; + error.message = + error.details?.[0]?.message || error.message || "Validation Error"; + next(error); + return; + } + + try { + const invite = await req.db.getInviteToken(req.body.token); + res + .status(200) + .json({ status: "success", msg: "Invite verified", data: invite }); + } catch (error) { + error.service = SERVICE_NAME; + next(error); + return; + } +}; + +module.exports = { + inviteController, + inviteVerifyController, +}; diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index 37aff6c96..f33ddcf10 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -1,10 +1,11 @@ const { getMonitorByIdParamValidation, getMonitorByIdQueryValidation, - getMonitorsByUserIdValidation, + getMonitorsByTeamIdValidation, createMonitorBodyValidation, editMonitorBodyValidation, - getMonitorsByUserIdQueryValidation, + getMonitorsByTeamIdQueryValidation, + pauseMonitorParamValidation, } = require("../validation/joi"); const sslChecker = require("ssl-checker"); @@ -123,7 +124,7 @@ const getMonitorById = async (req, res, next) => { } try { - const monitor = await req.db.getMonitorById(req, res); + const monitor = await req.db.getMonitorById(req.params.monitorId); if (!monitor) { const error = new Error(errorMessages.MONITOR_GET_BY_ID); error.status = 404; @@ -149,10 +150,10 @@ const getMonitorById = async (req, res, next) => { * @returns {Promise} * @throws {Error} */ -const getMonitorsByUserId = async (req, res, next) => { +const getMonitorsByTeamId = async (req, res, next) => { try { - await getMonitorsByUserIdValidation.validateAsync(req.params); - await getMonitorsByUserIdQueryValidation.validateAsync(req.query); + await getMonitorsByTeamIdValidation.validateAsync(req.params); + await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); } catch (error) { error.status = 422; error.service = SERVICE_NAME; @@ -163,12 +164,12 @@ const getMonitorsByUserId = async (req, res, next) => { } try { - const userId = req.params.userId; - const monitors = await req.db.getMonitorsByUserId(req, res); + const teamId = req.params.teamId; + const monitors = await req.db.getMonitorsByTeamId(req, res); return res.json({ success: true, - msg: successMessages.MONITOR_GET_BY_USER_ID(userId), + msg: successMessages.MONITOR_GET_BY_USER_ID(teamId), data: monitors, }); } catch (error) { @@ -204,12 +205,14 @@ const createMonitor = async (req, res, next) => { const monitor = await req.db.createMonitor(req, res); if (notifications && notifications.length !== 0) { - await Promise.all( + const setNotifications = await Promise.all( notifications.map(async (notification) => { notification.monitorId = monitor._id; await req.db.createNotification(notification); }) ); + monitor.notifications = setNotifications; + await monitor.save(); } // Add monitor to job queue req.jobQueue.addJob(monitor._id, monitor); @@ -309,7 +312,7 @@ const editMonitor = async (req, res, next) => { // Get notifications from the request body const notifications = req.body.notifications; - const editedMonitor = await req.db.editMonitor(req, res); + const editedMonitor = await req.db.editMonitor(monitorId, req.body); await req.db.deleteNotificationsByMonitorId(editedMonitor._id); @@ -337,14 +340,49 @@ const editMonitor = async (req, res, next) => { } }; +const pauseMonitor = async (req, res, next) => { + try { + await pauseMonitorParamValidation.validateAsync(req.params); + } catch (error) { + error.status = 422; + error.service = SERVICE_NAME; + error.message = + error.details?.[0]?.message || error.message || "Validation Error"; + next(error); + } + + try { + const monitor = await req.db.getMonitorById(req.params.monitorId); + if (monitor.isActive) { + await req.jobQueue.deleteJob(monitor); + } else { + await req.jobQueue.addJob(monitor._id, monitor); + } + monitor.isActive = !monitor.isActive; + monitor.save(); + return res.status(200).json({ + success: true, + msg: monitor.isActive + ? successMessages.MONITOR_RESUME + : successMessages.MONITOR_PAUSE, + data: monitor, + }); + } catch (error) { + error.service = SERVICE_NAME; + error.method = "pauseMonitor"; + next(error); + } +}; + module.exports = { getAllMonitors, getMonitorStatsById, getMonitorCertificate, getMonitorById, - getMonitorsByUserId, + getMonitorsByTeamId, createMonitor, deleteMonitor, deleteAllMonitors, editMonitor, + pauseMonitor, }; diff --git a/Server/db/mongo/MongoDB.js b/Server/db/mongo/MongoDB.js index 9c1005c32..912bfb438 100644 --- a/Server/db/mongo/MongoDB.js +++ b/Server/db/mongo/MongoDB.js @@ -15,10 +15,10 @@ const connect = async () => { } }; -const checkAdmin = async (req, res) => { +const checkSuperadmin = async (req, res) => { try { - const admin = await UserModel.findOne({ role: "admin" }); - if (admin !== null) { + const superAdmin = await UserModel.findOne({ role: "superadmin" }); + if (superAdmin !== null) { return true; } return false; @@ -66,7 +66,7 @@ const { getAllMonitors, getMonitorStatsById, getMonitorById, - getMonitorsByUserId, + getMonitorsByTeamId, createMonitor, deleteMonitor, deleteAllMonitors, @@ -92,8 +92,9 @@ const { createCheck, getChecksCount, getChecks, - getUserChecks, + getTeamChecks, deleteChecks, + deleteChecksByTeamId, } = require("./modules/checkModule"); //**************************************** @@ -141,11 +142,11 @@ module.exports = { requestRecoveryToken, validateRecoveryToken, resetPassword, - checkAdmin, + checkSuperadmin, getAllMonitors, getMonitorStatsById, getMonitorById, - getMonitorsByUserId, + getMonitorsByTeamId, createMonitor, deleteMonitor, deleteAllMonitors, @@ -153,8 +154,9 @@ module.exports = { createCheck, getChecksCount, getChecks, - getUserChecks, + getTeamChecks, deleteChecks, + deleteChecksByTeamId, createAlert, getAlertsByUserId, getAlertsByMonitorId, diff --git a/Server/db/mongo/modules/checkModule.js b/Server/db/mongo/modules/checkModule.js index 782e5f64d..32968431d 100644 --- a/Server/db/mongo/modules/checkModule.js +++ b/Server/db/mongo/modules/checkModule.js @@ -22,7 +22,27 @@ const dateRangeLookup = { const createCheck = async (checkData) => { try { + const { monitorId, status } = checkData; + + const n = await Check.countDocuments({ monitorId }) + 1; + const check = await new Check({ ...checkData }).save(); + + const monitor = await Monitor.findById(monitorId); + + if (!monitor) { + throw new Error("Monitor not found"); + } + + if (monitor.uptimePercentage === undefined) { + monitor.uptimePercentage = (status === true) ? 1 : 0; + } else { + monitor.uptimePercentage = + (monitor.uptimePercentage * (n - 1) + (status === true ? 1: 0)) / n; + } + + await monitor.save(); + return check; } catch (error) { throw error; @@ -116,12 +136,12 @@ const getChecks = async (req) => { } }; -const getUserChecks = async (req) => { - const { userId } = req.params; +const getTeamChecks = async (req) => { + const { teamId } = req.params; let { sortOrder, limit, dateRange, filter, page, rowsPerPage } = req.query; // Get monitorIDs - const userMonitors = await Monitor.find({ userId: userId }); + const userMonitors = await Monitor.find({ teamId: teamId }); const monitorIds = userMonitors.map((monitor) => monitor._id); //Build check query @@ -170,7 +190,7 @@ const getUserChecks = async (req) => { }; /** - * Delete all checks for a user + * Delete all checks for a monitor * @async * @param {string} monitorId * @returns {number} @@ -185,10 +205,40 @@ const deleteChecks = async (monitorId) => { throw error; } }; + +/** + * Delete all checks for a team + * @async + * @param {string} monitorId + * @returns {number} + * @throws {Error} + */ + +const deleteChecksByTeamId = async (teamId) => { + try { + const teamMonitors = await Monitor.find({ teamId: teamId }); + let totalDeletedCount = 0; + + await Promise.all( + teamMonitors.map(async (monitor) => { + const result = await Check.deleteMany({ monitorId: monitor._id }); + totalDeletedCount += result.deletedCount; + monitor.status = true; + await monitor.save(); + }) + ); + + return totalDeletedCount; + } catch (error) { + throw error; + } +}; + module.exports = { createCheck, getChecksCount, getChecks, - getUserChecks, + getTeamChecks, deleteChecks, + deleteChecksByTeamId, }; diff --git a/Server/db/mongo/modules/inviteModule.js b/Server/db/mongo/modules/inviteModule.js index b4404c615..975f6f1e8 100644 --- a/Server/db/mongo/modules/inviteModule.js +++ b/Server/db/mongo/modules/inviteModule.js @@ -2,14 +2,25 @@ const InviteToken = require("../../../models/InviteToken"); const crypto = require("crypto"); const { errorMessages } = require("../../../utils/messages"); -const requestInviteToken = async (req, res) => { +/** + * Request an invite token for a user. + * + * This function deletes any existing invite tokens for the user's email, + * generates a new token, saves it, and then returns the new token. + * + * @param {Object} userData - The user data. + * @param {string} userData.email - The user's email. + * @param {mongoose.Schema.Types.ObjectId} userData.teamId - The ID of the team. + * @param {Array} userData.role - The user's role(s). + * @param {Date} [userData.expiry=Date.now] - The expiry date of the token. Defaults to the current date and time. + * @returns {Promise} The invite token. + * @throws {Error} If there is an error. + */ +const requestInviteToken = async (userData) => { try { - await InviteToken.deleteMany({ email: req.body.email }); - let inviteToken = new InviteToken({ - email: req.body.email, - role: req.body.role, - token: crypto.randomBytes(32).toString("hex"), - }); + await InviteToken.deleteMany({ email: userData.email }); + userData.token = crypto.randomBytes(32).toString("hex"); + let inviteToken = new InviteToken(userData); await inviteToken.save(); return inviteToken; } catch (error) { @@ -17,10 +28,20 @@ const requestInviteToken = async (req, res) => { } }; -const getInviteToken = async (req, res) => { +/** + * Retrieves and deletes an invite token. + * + * This function searches for an invite token in the database and deletes it. + * If the invite token is not found, it throws an error. + * + * @param {string} token - The invite token to search for. + * @returns {Promise} The invite token data. + * @throws {Error} If the invite token is not found or there is another error. + */ +const getInviteToken = async (token) => { try { const invite = await InviteToken.findOneAndDelete({ - token: req.body.token, + token, }); if (invite === null) { throw new Error(errorMessages.AUTH_INVITE_NOT_FOUND); diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index 9778b6224..79e8917ea 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -15,7 +15,7 @@ const { NormalizeData } = require("../../../utils/dataUtils"); */ const getAllMonitors = async (req, res) => { try { - const monitors = await Monitor.find({ isActive: true }); + const monitors = await Monitor.find(); return monitors; } catch (error) { throw error; @@ -280,24 +280,33 @@ const getMonitorStatsById = async (req) => { const getMonitorById = async (monitorId) => { try { const monitor = await Monitor.findById(monitorId); - return monitor; + if (monitor === null || monitor === undefined) { + throw new Error(errorMessages.DB_FIND_MONTIOR_BY_ID(monitorId)); + } + // Get notifications + const notifications = await Notification.find({ + monitorId: monitorId, + }); + monitor.notifications = notifications; + const monitorWithNotifications = await monitor.save(); + return monitorWithNotifications; } catch (error) { throw error; } }; /** - * Get monitors by UserID + * Get monitors by TeamID * @async * @param {Express.Request} req * @param {Express.Response} res * @returns {Promise>} * @throws {Error} */ -const getMonitorsByUserId = async (req, res) => { +const getMonitorsByTeamId = async (req, res) => { try { let { limit, type, status, sortOrder, normalize } = req.query || {}; - const monitorQuery = { userId: req.params.userId }; + const monitorQuery = { teamId: req.params.teamId }; if (type !== undefined) { const types = Array.isArray(type) ? type : [type]; @@ -340,12 +349,7 @@ const getMonitorsByUserId = async (req, res) => { if (normalize !== undefined) { checks = NormalizeData(checks, 10, 100); } - - // Get notifications - const notifications = await Notification.find({ - monitorId: monitor._id, - }); - return { ...monitor.toObject(), checks, notifications }; + return { ...monitor.toObject(), checks }; }) ); return monitorsWithChecks; @@ -367,7 +371,6 @@ const createMonitor = async (req, res) => { const monitor = new Monitor({ ...req.body }); // Remove notifications fom monitor as they aren't needed here monitor.notifications = undefined; - monitor.userId = req.user._id; await monitor.save(); return monitor; } catch (error) { @@ -432,9 +435,7 @@ const deleteMonitorsByUserId = async (userId) => { * @returns {Promise} * @throws {Error} */ -const editMonitor = async (req, res) => { - const candidateId = req.params.monitorId; - const candidateMonitor = req.body; +const editMonitor = async (candidateId, candidateMonitor) => { candidateMonitor.notifications = undefined; try { @@ -453,7 +454,7 @@ module.exports = { getAllMonitors, getMonitorStatsById, getMonitorById, - getMonitorsByUserId, + getMonitorsByTeamId, createMonitor, deleteMonitor, deleteAllMonitors, diff --git a/Server/db/mongo/modules/userModule.js b/Server/db/mongo/modules/userModule.js index 3dacfd943..1a5c19f36 100644 --- a/Server/db/mongo/modules/userModule.js +++ b/Server/db/mongo/modules/userModule.js @@ -1,4 +1,5 @@ const UserModel = require("../../../models/user"); +const TeamModel = require("../../../models/Team"); const { errorMessages } = require("../../../utils/messages"); const { GenerateAvatarImage } = require("../../../utils/imageProcessing"); @@ -13,20 +14,29 @@ const { ParseBoolean } = require("../../../utils/utils"); * @returns {Promise} * @throws {Error} */ -const insertUser = async (req, res) => { +const insertUser = async (userData, imageFile) => { try { - const userData = { ...req.body }; - if (req.file) { + if (imageFile) { // 1. Save the full size image userData.profileImage = { - data: req.file.buffer, - contentType: req.file.mimetype, + data: imageFile.buffer, + contentType: imageFile.mimetype, }; // 2. Get the avatar sized image - const avatar = await GenerateAvatarImage(req.file); + const avatar = await GenerateAvatarImage(imageFile); userData.avatarImage = avatar; } + + // Handle creating team if superadmin + if (userData.role.includes("superadmin")) { + const team = new TeamModel({ + email: userData.email, + }); + userData.teamId = team._id; + await team.save(); + } + const newUser = new UserModel(userData); await newUser.save(); return await UserModel.findOne({ _id: newUser._id }) @@ -79,7 +89,6 @@ const getUserByEmail = async (email) => { const updateUser = async (req, res) => { const candidateUserId = req.params.userId; - try { const candidateUser = { ...req.body }; // ****************************************** diff --git a/Server/index.js b/Server/index.js index 9a5f1797f..846b11e3d 100644 --- a/Server/index.js +++ b/Server/index.js @@ -7,6 +7,7 @@ const { verifyJWT } = require("./middleware/verifyJWT"); const { handleErrors } = require("./middleware/handleErrors"); const { errorMessages } = require("./utils/messages"); const authRouter = require("./routes/authRoute"); +const inviteRouter = require("./routes/inviteRoute"); const monitorRouter = require("./routes/monitorRoute"); const checkRouter = require("./routes/checkRoute"); const alertRouter = require("./routes/alertRoute"); @@ -71,6 +72,7 @@ const startApp = async () => { //routes app.use("/api/v1/auth", authRouter); + app.use("/api/v1/invite", inviteRouter); app.use("/api/v1/monitors", verifyJWT, monitorRouter); app.use("/api/v1/checks", verifyJWT, checkRouter); app.use("/api/v1/alerts", verifyJWT, alertRouter); diff --git a/Server/middleware/isAllowed.js b/Server/middleware/isAllowed.js new file mode 100644 index 000000000..fefb26099 --- /dev/null +++ b/Server/middleware/isAllowed.js @@ -0,0 +1,53 @@ +const jwt = require("jsonwebtoken"); +const TOKEN_PREFIX = "Bearer "; +const SERVICE_NAME = "allowedRoles"; +const { errorMessages } = require("../utils/messages"); + +const isAllowed = (allowedRoles) => { + return (req, res, next) => { + const token = req.headers["authorization"]; + + // If no token is pressent, return an error + if (!token) { + const error = new Error(errorMessages.NO_AUTH_TOKEN); + error.status = 401; + error.service = SERVICE_NAME; + next(error); + return; + } + + // If the token is improperly formatted, return an error + if (!token.startsWith(TOKEN_PREFIX)) { + const error = new Error(errorMessages.INVALID_AUTH_TOKEN); + error.status = 400; + error.service = SERVICE_NAME; + next(error); + return; + } + // Parse the token + try { + const parsedToken = token.slice(TOKEN_PREFIX.length, token.length); + var decoded = jwt.verify(parsedToken, process.env.JWT_SECRET); + const userRoles = decoded.role; + + // Check if the user has the required role + if (userRoles.some((role) => allowedRoles.includes(role))) { + next(); + return; + } else { + const error = new Error(errorMessages.INSUFFICIENT_PERMISSIONS); + error.status = 401; + error.service = SERVICE_NAME; + next(error); + return; + } + } catch (error) { + error.status = 401; + error.service = SERVICE_NAME; + next(error); + return; + } + }; +}; + +module.exports = { isAllowed }; diff --git a/Server/middleware/verifyAdmin.js b/Server/middleware/verifySuperAdmin.js similarity index 91% rename from Server/middleware/verifyAdmin.js rename to Server/middleware/verifySuperAdmin.js index 7ef123451..82401b3b7 100644 --- a/Server/middleware/verifyAdmin.js +++ b/Server/middleware/verifySuperAdmin.js @@ -11,7 +11,7 @@ const { errorMessages } = require("../utils/messages"); * @param {express.NextFunction} next * @returns {express.Response} */ -const verifyAdmin = (req, res, next) => { +const verifySuperAdmin = (req, res, next) => { const token = req.headers["authorization"]; // Make sure a token is provided if (!token) { @@ -42,7 +42,7 @@ const verifyAdmin = (req, res, next) => { .json({ success: false, msg: errorMessages.INVALID_AUTH_TOKEN }); } - if (decoded.role.includes("admin") === false) { + if (decoded.role.includes("superadmin") === false) { logger.error(errorMessages.INVALID_AUTH_TOKEN, { service: SERVICE_NAME, }); @@ -54,4 +54,4 @@ const verifyAdmin = (req, res, next) => { }); }; -module.exports = { verifyAdmin }; +module.exports = { verifySuperAdmin }; diff --git a/Server/models/InviteToken.js b/Server/models/InviteToken.js index 10515dadb..5fbd4fc73 100644 --- a/Server/models/InviteToken.js +++ b/Server/models/InviteToken.js @@ -7,6 +7,12 @@ const InviteTokenSchema = mongoose.Schema( required: true, unique: true, }, + teamId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Team", + immutable: true, + required: true, + }, role: { type: Array, required: true, diff --git a/Server/models/Monitor.js b/Server/models/Monitor.js index 7d4bd17d2..fdc147d1b 100644 --- a/Server/models/Monitor.js +++ b/Server/models/Monitor.js @@ -1,4 +1,5 @@ const mongoose = require("mongoose"); +const Notification = require("./Notification"); const MonitorSchema = mongoose.Schema( { @@ -6,6 +7,13 @@ const MonitorSchema = mongoose.Schema( type: mongoose.Schema.Types.ObjectId, ref: "User", immutable: true, + required: true, + }, + teamId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Team", + immutable: true, + required: true, }, name: { type: String, @@ -36,6 +44,16 @@ const MonitorSchema = mongoose.Schema( type: Number, default: 60000, }, + uptimePercentage: { + type: Number, + default: undefined, + }, + notifications: [ + { + type: mongoose.Schema.Types.ObjectId, + ref: "Notification", + }, + ], }, { timestamps: true, diff --git a/Server/models/Team.js b/Server/models/Team.js new file mode 100644 index 000000000..25e1f9b22 --- /dev/null +++ b/Server/models/Team.js @@ -0,0 +1,16 @@ +const mongoose = require("mongoose"); + +const TeamSchema = mongoose.Schema( + { + email: { + type: String, + required: true, + unique: true, + }, + }, + { + timestamps: true, + } +); + +module.exports = mongoose.model("Team", TeamSchema); diff --git a/Server/models/user.js b/Server/models/user.js index 977cabc15..829129b4d 100644 --- a/Server/models/user.js +++ b/Server/models/user.js @@ -38,7 +38,12 @@ const UserSchema = mongoose.Schema( role: { type: [String], default: "user", - enum: ["user", "admin"], + enum: ["user", "admin", "superadmin"], + }, + teamId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Team", + immutable: true, }, }, { @@ -70,4 +75,10 @@ UserSchema.methods.comparePassword = async function (submittedPassword) { return res; }; +const User = mongoose.model("User", UserSchema); + +User.init().then(() => { + console.log("User model initialized"); +}); + module.exports = mongoose.model("User", UserSchema); diff --git a/Server/routes/authRoute.js b/Server/routes/authRoute.js index 201807121..743a55c19 100644 --- a/Server/routes/authRoute.js +++ b/Server/routes/authRoute.js @@ -1,7 +1,7 @@ const router = require("express").Router(); const { verifyJWT } = require("../middleware/verifyJWT"); -const { verifyAdmin } = require("../middleware/verifyAdmin"); const { verifyOwnership } = require("../middleware/verifyOwnership"); +const { isAllowed } = require("../middleware/isAllowed"); const multer = require("multer"); const upload = multer(); const User = require("../models/user"); @@ -14,11 +14,9 @@ const { recoveryRequestController, validateRecoveryTokenController, resetPasswordController, - checkAdminController, + checkSuperadminController, getAllUsersController, deleteUserController, - inviteController, - inviteVerifyController, } = require("../controllers/authController"); //Auth routes @@ -30,8 +28,13 @@ router.put( verifyJWT, userEditController ); -router.get("/users/admin", checkAdminController); -router.get("/users", verifyJWT, verifyAdmin, getAllUsersController); +router.get("/users/superadmin", checkSuperadminController); +router.get( + "/users", + verifyJWT, + isAllowed(["admin", "superadmin"]), + getAllUsersController +); router.delete( "/user/:userId", verifyJWT, @@ -39,9 +42,6 @@ router.delete( deleteUserController ); -router.post("/invite", verifyJWT, inviteController); -router.post("/invite/verify", inviteVerifyController); - //Recovery routes router.post("/recovery/request", recoveryRequestController); router.post("/recovery/validate", validateRecoveryTokenController); diff --git a/Server/routes/checkRoute.js b/Server/routes/checkRoute.js index 4bba24e79..649a68ccf 100644 --- a/Server/routes/checkRoute.js +++ b/Server/routes/checkRoute.js @@ -1,6 +1,7 @@ const router = require("express").Router(); const checkController = require("../controllers/checkController"); const { verifyOwnership } = require("../middleware/verifyOwnership"); +const { isAllowed } = require("../middleware/isAllowed"); const Monitor = require("../models/Monitor"); router.post( @@ -9,13 +10,9 @@ router.post( checkController.createCheck ); -router.get( - "/:monitorId", - verifyOwnership(Monitor, "monitorId"), - checkController.getChecks -); +router.get("/:monitorId", checkController.getChecks); -router.get("/user/:userId", checkController.getUserChecks); +router.get("/team/:teamId", checkController.getTeamChecks); router.delete( "/:monitorId", @@ -23,4 +20,10 @@ router.delete( checkController.deleteChecks ); +router.delete( + "/team/:teamId", + isAllowed(["admin", "superadmin"]), + checkController.deleteChecksByTeamId +); + module.exports = router; diff --git a/Server/routes/inviteRoute.js b/Server/routes/inviteRoute.js new file mode 100644 index 000000000..54e74b0fa --- /dev/null +++ b/Server/routes/inviteRoute.js @@ -0,0 +1,22 @@ +const router = require("express").Router(); +const { verifyJWT } = require("../middleware/verifyJWT"); +const { isAllowed } = require("../middleware/isAllowed"); + +const { + inviteController, + inviteVerifyController, +} = require("../controllers/inviteController"); + +router.post( + "/", + isAllowed(["admin", "superadmin"]), + verifyJWT, + inviteController +); +router.post( + "/verify", + isAllowed(["admin", "superadmin"]), + inviteVerifyController +); + +module.exports = router; diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js index 847b2b25d..ee1c08488 100644 --- a/Server/routes/monitorRoute.js +++ b/Server/routes/monitorRoute.js @@ -1,25 +1,41 @@ const router = require("express").Router(); const monitorController = require("../controllers/monitorController"); -const { verifyOwnership } = require("../middleware/verifyOwnership"); -const Monitor = require("../models/Monitor"); +const { isAllowed } = require("../middleware/isAllowed"); 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); +router.get("/team/:teamId", monitorController.getMonitorsByTeamId); + +router.post( + "/", + isAllowed(["admin", "superadmin"]), + monitorController.createMonitor +); -router.post("/", monitorController.createMonitor); router.delete( "/:monitorId", - verifyOwnership(Monitor, "monitorId"), + isAllowed(["admin", "superadmin"]), monitorController.deleteMonitor ); + router.put( "/:monitorId", - verifyOwnership(Monitor, "monitorId"), + isAllowed(["admin", "superadmin"]), monitorController.editMonitor ); -router.delete("/all", monitorController.deleteAllMonitors); +router.delete( + "/all", + isAllowed(["superadmin"]), + monitorController.deleteAllMonitors +); + +router.post( + "/pause/:monitorId", + isAllowed(["admin", "superadmin"]), + monitorController.pauseMonitor +); + module.exports = router; diff --git a/Server/service/networkService.js b/Server/service/networkService.js index b688b6fff..f5c333a60 100644 --- a/Server/service/networkService.js +++ b/Server/service/networkService.js @@ -1,6 +1,8 @@ const axios = require("axios"); const ping = require("ping"); const logger = require("../utils/logger"); +const http = require("http"); +const { errorMessages, successMessages } = require("../utils/messages"); class NetworkService { constructor(db, emailService) { @@ -14,37 +16,45 @@ class NetworkService { } async handleNotification(job, isAlive) { - const { _id } = job.data; - const monitor = await this.db.getMonitorById(_id); - if (monitor === null || monitor === undefined) { - logger.error(`Null Monitor: ${_id}`, { + try { + const { _id } = job.data; + const monitor = await this.db.getMonitorById(_id); + if (monitor === null || monitor === undefined) { + logger.error(`Null Monitor: ${_id}`, { + method: "handleNotification", + service: this.SERVICE_NAME, + jobId: job.id, + }); + return; + } + + // 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}` + ); + } + } + } + } catch (error) { + logger.error(error.message, { method: "handleNotification", service: this.SERVICE_NAME, jobId: job.id, }); - return; - } - - // 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}` - ); - } - } } } @@ -84,11 +94,13 @@ class NetworkService { const { responseTime, response } = await this.measureResponseTime(operation); isAlive = response.alive; - const checkData = { monitorId: job.data._id, status: isAlive, responseTime, + message: isAlive + ? successMessages.PING_SUCCESS + : errorMessages.PING_CANNOT_RESOLVE, }; return await this.logAndStoreCheck(checkData, this.db.createCheck); } catch (error) { @@ -96,6 +108,7 @@ class NetworkService { const checkData = { monitorId: job.data._id, status: isAlive, + message: errorMessages.PING_CANNOT_RESOLVE, responseTime: error.responseTime, }; return await this.logAndStoreCheck(checkData, this.db.createCheck); @@ -122,7 +135,6 @@ class NetworkService { try { const { responseTime, response } = await this.measureResponseTime(operation); - // check if response is in the 200 range, if so, service is up isAlive = response.status >= 200 && response.status < 300; @@ -132,21 +144,21 @@ class NetworkService { status: isAlive, responseTime, statusCode: response.status, + message: http.STATUS_CODES[response.status], }; return await this.logAndStoreCheck(checkData, this.db.createCheck); } catch (error) { + const statusCode = error.response?.status || this.NETWORK_ERROR; + let message = http.STATUS_CODES[statusCode] || "Network Error"; isAlive = false; const checkData = { monitorId: job.data._id, status: isAlive, + statusCode, responseTime: error.responseTime, + message, }; - // The server returned a response - if (error.response) { - checkData.statusCode = error.response.status; - } else { - checkData.statusCode = this.NETWORK_ERROR; - } + return await this.logAndStoreCheck(checkData, this.db.createCheck); } finally { this.handleNotification(job, isAlive); @@ -197,6 +209,8 @@ class NetworkService { const checkData = { monitorId: job.data._id, status: isAlive, + statusCode: response.status, + message: http.STATUS_CODES[response.status], accessibility: (categories.accessibility?.score || 0) * 100, bestPractices: (categories["best-practices"]?.score || 0) * 100, seo: (categories.seo?.score || 0) * 100, @@ -213,9 +227,13 @@ class NetworkService { this.logAndStoreCheck(checkData, this.db.createPageSpeedCheck); } catch (error) { isAlive = false; + const statusCode = error.response?.status || this.NETWORK_ERROR; + const message = http.STATUS_CODES[statusCode] || "Network Error"; const checkData = { monitorId: job.data._id, status: isAlive, + statusCode, + message, accessibility: 0, bestPractices: 0, seo: 0, diff --git a/Server/utils/messages.js b/Server/utils/messages.js index 55f0fa7e9..53a8f689f 100644 --- a/Server/utils/messages.js +++ b/Server/utils/messages.js @@ -17,6 +17,9 @@ const errorMessages = { VERIFY_OWNER_NOT_FOUND: "Document not found", VERIFY_OWNER_UNAUTHORIZED: "Unauthorized access", + //Permissions Middleware + INSUFFICIENT_PERMISSIONS: "Insufficient permissions", + //DB Errors DB_USER_EXISTS: "User already exists", DB_USER_NOT_FOUND: "User not found", @@ -41,6 +44,9 @@ const errorMessages = { JOB_QUEUE_WORKER_CLOSE: "Error closing worker", JOB_QUEUE_DELETE_JOB: "Job not found in queue", JOB_QUEUE_OBLITERATE: "Error obliterating queue", + + // PING Operations + PING_CANNOT_RESOLVE: "No response", }; const successMessages = { @@ -81,10 +87,15 @@ const successMessages = { //Job Queue JOB_QUEUE_DELETE_JOB: "Job removed successfully", JOB_QUEUE_OBLITERATE: "Queue OBLITERATED!!!", + JOB_QUEUE_PAUSE_JOB: "Job paused successfully", + JOB_QUEUE_RESUME_JOB: "Job resumed successfully", //Maintenance Window Controller MAINTENANCE_WINDOW_CREATE: "Maintenance Window created successfully", MAINTEANCE_WINDOW_GET_BY_USER: "Got Maintenance Windows by User successfully", + + //Ping Operations + PING_SUCCESS: "Success", }; module.exports = { diff --git a/Server/validation/joi.js b/Server/validation/joi.js index 4f8042e12..ccc8da9f6 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -5,8 +5,11 @@ const joi = require("joi"); //**************************************** const roleValidatior = (role) => (value, helpers) => { - if (!value.includes(role)) { - throw new joi.ValidationError(`You do not have ${role} authorization`); + const hasRole = role.some((role) => value.includes(role)); + if (!hasRole) { + throw new Joi.ValidationError( + `You do not have the required authorization. Required roles: ${roles.join(", ")}` + ); } return value; }; @@ -36,7 +39,7 @@ const loginValidation = joi.object({ ), }); -const registerValidation = joi.object({ +const registrationBodyValidation = joi.object({ firstName: joi .string() .required() @@ -66,9 +69,10 @@ const registerValidation = joi.object({ profileImage: joi.any(), role: joi .array() - .items(joi.string().valid("admin", "user")) + .items(joi.string().valid("superadmin", "admin", "user")) .min(1) .required(), + teamId: joi.string().allow("").required(), }); const editUserParamValidation = joi.object({ @@ -123,7 +127,7 @@ const deleteUserParamValidation = joi.object({ }); const inviteRoleValidation = joi.object({ - roles: joi.custom(roleValidatior("admin")).required(), + roles: joi.custom(roleValidatior(["admin", "superadmin"])).required(), }); const inviteBodyValidation = joi.object({ @@ -132,6 +136,7 @@ const inviteBodyValidation = joi.object({ "string.email": "Must be a valid email address", }), role: joi.array().required(), + teamId: joi.string().required(), }); const inviteVerifciationBodyValidation = joi.object({ @@ -155,11 +160,11 @@ const getMonitorByIdQueryValidation = joi.object({ normalize: joi.boolean(), }); -const getMonitorsByUserIdValidation = joi.object({ - userId: joi.string().required(), +const getMonitorsByTeamIdValidation = joi.object({ + teamId: joi.string().required(), }); -const getMonitorsByUserIdQueryValidation = joi.object({ +const getMonitorsByTeamIdQueryValidation = joi.object({ status: joi.boolean(), sortOrder: joi.string().valid("asc", "desc"), limit: joi.number(), @@ -175,6 +180,7 @@ const getMonitorsByUserIdQueryValidation = joi.object({ const createMonitorBodyValidation = joi.object({ _id: joi.string(), userId: joi.string().required(), + teamId: joi.string().required(), name: joi.string().required(), description: joi.string().required(), type: joi.string().required(), @@ -191,6 +197,10 @@ const editMonitorBodyValidation = joi.object({ notifications: joi.array().items(joi.object()), }); +const pauseMonitorParamValidation = joi.object({ + monitorId: joi.string().required(), +}); + //**************************************** // Alerts //**************************************** @@ -265,11 +275,11 @@ const getChecksQueryValidation = joi.object({ rowsPerPage: joi.number(), }); -const getUserChecksParamValidation = joi.object({ - userId: joi.string().required(), +const getTeamChecksParamValidation = joi.object({ + teamId: joi.string().required(), }); -const getUserChecksQueryValidation = joi.object({ +const getTeamChecksQueryValidation = joi.object({ sortOrder: joi.string().valid("asc", "desc"), limit: joi.number(), dateRange: joi.string().valid("day", "week", "month"), @@ -282,6 +292,10 @@ const deleteChecksParamValidation = joi.object({ monitorId: joi.string().required(), }); +const deleteChecksByTeamIdParamValidation = joi.object({ + teamId: joi.string().required(), +}); + //**************************************** // PageSpeedCheckValidation //**************************************** @@ -331,7 +345,7 @@ const getMaintenanceWindowsByMonitorIdParamValidation = joi.object({ module.exports = { roleValidatior, loginValidation, - registerValidation, + registrationBodyValidation, recoveryValidation, recoveryTokenValidation, newPasswordValidation, @@ -341,9 +355,10 @@ module.exports = { createMonitorBodyValidation, getMonitorByIdParamValidation, getMonitorByIdQueryValidation, - getMonitorsByUserIdValidation, - getMonitorsByUserIdQueryValidation, + getMonitorsByTeamIdValidation, + getMonitorsByTeamIdQueryValidation, editMonitorBodyValidation, + pauseMonitorParamValidation, editUserParamValidation, editUserBodyValidation, createAlertParamValidation, @@ -358,9 +373,10 @@ module.exports = { createCheckBodyValidation, getChecksParamValidation, getChecksQueryValidation, - getUserChecksParamValidation, - getUserChecksQueryValidation, + getTeamChecksParamValidation, + getTeamChecksQueryValidation, deleteChecksParamValidation, + deleteChecksByTeamIdParamValidation, deleteUserParamValidation, getPageSpeedCheckParamValidation, createPageSpeedCheckParamValidation,