Merge branch 'develop' into rog404/http-status-label

This commit is contained in:
Rogerio Bordignon authored and GitHub committed 2024-10-25 11:28:55 -03:00
commit fd88df5a59
64 files changed
+3704 -2083

No files matched your search

+1
View File
@@ -14,5 +14,6 @@ module.exports = {
rules: {
"react/jsx-no-target-blank": "off",
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
"react/no-unescaped-entities": "off",
},
};
+445 -453
View File
File diff suppressed because it is too large. Load diff
+7 -7
View File
@@ -17,13 +17,13 @@
"@mui/lab": "^5.0.0-alpha.170",
"@mui/material": "^5.15.16",
"@mui/x-charts": "^7.5.1",
"@mui/x-data-grid": "7.3.2",
"@mui/x-date-pickers": "7.3.2",
"@reduxjs/toolkit": "2.2.5",
"@mui/x-data-grid": "7.21.0",
"@mui/x-date-pickers": "7.21.0",
"@reduxjs/toolkit": "2.3.0",
"axios": "^1.7.4",
"chart.js": "^4.4.3",
"dayjs": "1.11.11",
"joi": "17.13.1",
"dayjs": "1.11.13",
"joi": "17.13.3",
"jwt-decode": "^4.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@@ -31,7 +31,7 @@
"react-router": "^6.23.0",
"react-router-dom": "^6.23.1",
"react-toastify": "^10.0.5",
"recharts": "2.13.0-alpha.4",
"recharts": "2.13.0",
"redux-persist": "6.0.0",
"vite-plugin-svgr": "^4.2.0"
},
@@ -41,7 +41,7 @@
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.6",
"prettier": "^3.3.3",
"vite": "^5.2.0"
+16 -1
View File
@@ -36,7 +36,7 @@ import { useSelector } from "react-redux";
import { CssBaseline } from "@mui/material";
import { useEffect } from "react";
import { useDispatch } from "react-redux";
import { getAppSettings } from "./Features/Settings/settingsSlice";
import { getAppSettings, updateAppSettings } from "./Features/Settings/settingsSlice";
import { logger } from "./Utils/Logger"; // Import the logger
import { networkService } from "./main";
function App() {
@@ -66,6 +66,21 @@ function App() {
};
}, []);
useEffect(() => {
const thing = async () => {
const action = await dispatch(
updateAppSettings({ authToken, settings: { apiBaseUrl: "test" } })
);
if (action.payload.success) {
console.log(action.payload.data);
} else {
console.log(action);
}
};
thing();
}, [dispatch, authToken]);
return (
<ThemeProvider theme={mode === "light" ? lightTheme : darkTheme}>
<CssBaseline />
@@ -0,0 +1,68 @@
import { useId } from "react";
import PropTypes from "prop-types";
import { Modal, Stack, Typography } from "@mui/material";
const GenericDialog = ({ title, description, open, onClose, theme, children }) => {
const titleId = useId();
const descriptionId = useId();
const ariaDescribedBy = description?.length > 0 ? descriptionId : "";
return (
<Modal
aria-labelledby={titleId}
aria-describedby={ariaDescribedBy}
open={open}
onClose={onClose}
onClick={(e)=>e.stopPropagation()}
>
<Stack
gap={theme.spacing(2)}
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 400,
bgcolor: theme.palette.background.main,
border: 1,
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
boxShadow: 24,
p: theme.spacing(15),
"&:focus": {
outline: "none",
},
}}
>
<Typography
id={titleId}
component="h2"
fontSize={16}
color={theme.palette.text.primary}
fontWeight={600}
>
{title}
</Typography>
{description && (
<Typography
id={descriptionId}
color={theme.palette.text.tertiary}
>
{description}
</Typography>
)}
{children}
</Stack>
</Modal>
);
};
GenericDialog.propTypes = {
title: PropTypes.string.isRequired,
description: PropTypes.string,
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
children: PropTypes.element.isRequired,
};
export { GenericDialog };
+38 -79
View File
@@ -1,103 +1,62 @@
import LoadingButton from "@mui/lab/LoadingButton";
import { Button, Modal, Stack, Typography } from "@mui/material";
import PropTypes from "prop-types";
import LoadingButton from "@mui/lab/LoadingButton";
import { Button, Stack } from "@mui/material";
import { GenericDialog } from "./genericDialog";
const Dialog = ({
modelTitle,
modelDescription,
open,
onClose,
title,
confirmationBtnLbl,
confirmationBtnOnClick,
cancelBtnLbl,
cancelBtnOnClick,
theme,
isLoading,
description,
open,
theme,
onCancel,
confirmationButtonLabel,
onConfirm,
isLoading,
}) => {
return (
<Modal
aria-labelledby={modelTitle}
aria-describedby={modelDescription}
<GenericDialog
title={title}
description={description}
open={open}
onClose={onClose}
onClose={onCancel}
theme={theme}
>
<Stack
gap={theme.spacing(2)}
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 400,
bgcolor: theme.palette.background.main,
border: 1,
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
boxShadow: 24,
p: theme.spacing(15),
"&:focus": {
outline: "none",
},
}}
direction="row"
gap={theme.spacing(4)}
mt={theme.spacing(12)}
justifyContent="flex-end"
>
<Typography
id={modelTitle}
component="h2"
fontSize={16}
color={theme.palette.text.primary}
fontWeight={600}
<Button
variant="text"
color="info"
onClick={onCancel}
>
{title}
</Typography>
{description && (
<Typography
id={modelDescription}
color={theme.palette.text.tertiary}
>
{description}
</Typography>
)}
<Stack
direction="row"
gap={theme.spacing(4)}
mt={theme.spacing(12)}
justifyContent="flex-end"
Cancel
</Button>
<LoadingButton
variant="contained"
color="error"
loading={isLoading}
onClick={onConfirm}
>
<Button
variant="text"
color="info"
onClick={cancelBtnOnClick}
>
{cancelBtnLbl}
</Button>
<LoadingButton
variant="contained"
color="error"
loading={isLoading}
onClick={confirmationBtnOnClick}
>
{confirmationBtnLbl}
</LoadingButton>
</Stack>
{confirmationButtonLabel}
</LoadingButton>
</Stack>
</Modal>
</GenericDialog>
);
};
Dialog.propTypes = {
modelTitle: PropTypes.string.isRequired,
modelDescription: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
description: PropTypes.string,
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
title: PropTypes.string.isRequired,
confirmationBtnLbl: PropTypes.string.isRequired,
confirmationBtnOnClick: PropTypes.func.isRequired,
cancelBtnLbl: PropTypes.string.isRequired,
cancelBtnOnClick: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
onCancel: PropTypes.func.isRequired,
confirmationButtonLabel: PropTypes.string.isRequired,
onConfirm: PropTypes.func.isRequired,
isLoading: PropTypes.bool.isRequired,
description: PropTypes.string,
};
export default Dialog;
@@ -38,6 +38,7 @@ const SearchAdornment = () => {
);
};
//TODO keep search state inside of component
const Search = ({
id,
options,
+2
View File
@@ -181,6 +181,8 @@ function Sidebar() {
direction="row"
alignItems="center"
gap={theme.spacing(4)}
onClick={() => window.open("https://github.com/bluewave-labs/bluewave-uptime", "_blank", "noreferrer")}
sx={{ cursor: "pointer" }}
>
<Stack
justifyContent="center"
@@ -1,7 +1,7 @@
import { useTheme } from "@emotion/react";
import { useRef, useState } from "react";
import TabPanel from "@mui/lab/TabPanel";
import { Box, Button, Divider, Modal, Stack, Typography } from "@mui/material";
import { Box, Button, Divider, Stack, Typography } from "@mui/material";
import Avatar from "../../Avatar";
import Field from "../../Inputs/Field";
import ImageField from "../../Inputs/Image";
@@ -15,6 +15,8 @@ import { clearUptimeMonitorState } from "../../../Features/UptimeMonitors/uptime
import { createToast } from "../../../Utils/toastUtils";
import { logger } from "../../../Utils/Logger";
import LoadingButton from "@mui/lab/LoadingButton";
import { GenericDialog } from "../../Dialog/genericDialog";
import Dialog from "../../Dialog";
/**
* ProfilePanel component displays a form for editing user profile information
@@ -369,158 +371,82 @@ const ProfilePanel = () => {
</Box>
)}
{/* TODO - Update ModalPopup Component with @mui for reusability */}
<Modal
aria-labelledby="modal-delete-account"
aria-describedby="delete-account-confirmation"
<Dialog
open={isModalOpen("delete")}
onClose={() => setIsOpen("")}
disablePortal
>
<Stack
gap={theme.spacing(5)}
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 500,
bgcolor: theme.palette.background.main,
border: 1,
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
boxShadow: 24,
p: theme.spacing(15),
"&:focus": {
outline: "none",
},
}}
>
<Typography
id="modal-delete-account"
component="h1"
>
Really delete this account?
</Typography>
<Typography
id="delete-account-confirmation"
component="p"
>
If you delete your account, you will no longer be able to sign in, and all of
your data will be deleted. Deleting your account is permanent and
non-recoverable action.
</Typography>
<Stack
direction="row"
gap={theme.spacing(5)}
mt={theme.spacing(5)}
justifyContent="flex-end"
>
<Button
variant="text"
color="info"
onClick={() => setIsOpen("")}
>
Cancel
</Button>
<LoadingButton
variant="contained"
color="error"
onClick={handleDeleteAccount}
loading={isLoading}
>
Delete account
</LoadingButton>
</Stack>
</Stack>
</Modal>
<Modal
aria-labelledby="modal-update-picture"
aria-describedby="update-profile-picture"
theme={theme}
title={"Really delete this account?"}
description={
"If you delete your account, you will no longer be able to sign in, and all of your data will be deleted. Deleting your account is permanent and non-recoverable action."
}
onCancel={() => setIsOpen("")}
confirmationButtonLabel={"Delete account"}
onConfirm={handleDeleteAccount}
isLoading={isLoading}
/>
<GenericDialog
title={"Upload Image"}
open={isModalOpen("picture")}
onClose={closePictureModal}
theme={theme}
>
<Stack
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 475,
bgcolor: theme.palette.background.main,
border: 1,
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shape.boxShadow,
p: theme.spacing(15),
"&:focus": {
outline: "none",
},
}}
>
<Typography
id="modal-update-picture"
component="h1"
color={theme.palette.text.secondary}
>
Upload Image
</Typography>
<ImageField
id="update-profile-picture"
src={
file?.delete
? ""
: file?.src
? file.src
: localData?.file
? localData.file
: user?.avatarImage
? `data:image/png;base64,${user.avatarImage}`
: ""
}
loading={progress.isLoading && progress.value !== 100}
onChange={handlePicture}
<ImageField
id="update-profile-picture"
src={
file?.delete
? ""
: file?.src
? file.src
: localData?.file
? localData.file
: user?.avatarImage
? `data:image/png;base64,${user.avatarImage}`
: ""
}
loading={progress.isLoading && progress.value !== 100}
onChange={handlePicture}
/>
{progress.isLoading || progress.value !== 0 || errors["picture"] ? (
<ProgressUpload
icon={<ImageIcon />}
label={file?.name}
size={file?.size}
progress={progress.value}
onClick={removePicture}
error={errors["picture"]}
/>
{progress.isLoading || progress.value !== 0 || errors["picture"] ? (
<ProgressUpload
icon={<ImageIcon />}
label={file?.name}
size={file?.size}
progress={progress.value}
onClick={removePicture}
error={errors["picture"]}
/>
) : (
""
)}
<Stack
direction="row"
mt={theme.spacing(10)}
gap={theme.spacing(5)}
justifyContent="flex-end"
) : (
""
)}
<Stack
direction="row"
mt={theme.spacing(10)}
gap={theme.spacing(5)}
justifyContent="flex-end"
>
<Button
variant="text"
color="info"
onClick={removePicture}
>
<Button
variant="text"
color="info"
onClick={removePicture}
>
Remove
</Button>
<Button
variant="contained"
color="primary"
onClick={handleUpdatePicture}
disabled={
(Object.keys(errors).length !== 0 && errors?.picture) ||
progress.value !== 100
? true
: false
}
>
Update
</Button>
</Stack>
Remove
</Button>
<Button
variant="contained"
color="primary"
onClick={handleUpdatePicture}
disabled={
(Object.keys(errors).length !== 0 && errors?.picture) ||
progress.value !== 100
? true
: false
}
>
Update
</Button>
</Stack>
</Modal>
</GenericDialog>
</TabPanel>
);
};
@@ -1,6 +1,6 @@
import { useTheme } from "@emotion/react";
import TabPanel from "@mui/lab/TabPanel";
import { Box, Button, ButtonGroup, Modal, Stack, Typography } from "@mui/material";
import { Button, ButtonGroup, Stack, Typography } from "@mui/material";
import { useEffect, useState } from "react";
import Field from "../../Inputs/Field";
import { credentials } from "../../../Validation/validation";
@@ -10,6 +10,7 @@ import { useSelector } from "react-redux";
import BasicTable from "../../BasicTable";
import Select from "../../Inputs/Select";
import LoadingButton from "@mui/lab/LoadingButton";
import { GenericDialog } from "../../Dialog/genericDialog";
/**
* TeamPanel component manages the organization and team members,
@@ -31,10 +32,10 @@ const TeamPanel = () => {
const { authToken, user } = useSelector((state) => state.auth);
//TODO
const [orgStates, setOrgStates] = useState({
name: "Bluewave Labs",
isEdit: false,
});
// const [orgStates, setOrgStates] = useState({
// name: "Bluewave Labs",
// isEdit: false,
// });
const [toInvite, setToInvite] = useState({
email: "",
role: ["0"],
@@ -134,10 +135,10 @@ const TeamPanel = () => {
}, [errors, toInvite.email]);
// RENAME ORGANIZATION
const toggleEdit = () => {
setOrgStates((prev) => ({ ...prev, isEdit: !prev.isEdit }));
};
const handleRename = () => {};
// const toggleEdit = () => {
// setOrgStates((prev) => ({ ...prev, isEdit: !prev.isEdit }));
// };
// const handleRename = () => {};
// INVITE MEMBER
const [isOpen, setIsOpen] = useState(false);
@@ -327,100 +328,65 @@ const TeamPanel = () => {
table={"team"}
/>
</Stack>
<Modal
aria-labelledby="modal-invite-member"
aria-describedby="invite-member-to-team"
<GenericDialog
title={"Invite new team member"}
description={
"When you add a new team member, they will get access to all monitors."
}
open={isOpen}
onClose={closeInviteModal}
theme={theme}
>
<Field
type="email"
id="input-team-member"
placeholder="Email"
value={toInvite.email}
onChange={handleChange}
error={errors.email}
/>
<Select
id="team-member-role"
placeholder="Select role"
isHidden={true}
value={toInvite.role[0]}
onChange={(event) =>
setToInvite((prev) => ({
...prev,
role: [event.target.value],
}))
}
items={[
{ _id: "admin", name: "Admin" },
{ _id: "user", name: "User" },
]}
/>
<Stack
gap={theme.spacing(5)}
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 450,
bgcolor: theme.palette.background.main,
border: 1,
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shape.boxShadow,
p: theme.spacing(15),
"&:focus": {
outline: "none",
},
}}
direction="row"
gap={theme.spacing(4)}
mt={theme.spacing(8)}
justifyContent="flex-end"
>
<Box>
<Typography
id="modal-invite-member"
component="h1"
fontWeight={600}
fontColor={theme.palette.text.secondary}
>
Invite new team member
</Typography>
<Typography
id="invite-member-to-team"
component="p"
fontSize={13}
color={theme.palette.text.accent}
sx={{ mt: theme.spacing(1), mb: theme.spacing(4) }}
>
When you add a new team member, they will get access to all monitors.
</Typography>
</Box>
<Field
type="email"
id="input-team-member"
placeholder="Email"
value={toInvite.email}
onChange={handleChange}
error={errors.email}
/>
<Select
id="team-member-role"
placeholder="Select role"
isHidden={true}
value={toInvite.role[0]}
onChange={(event) =>
setToInvite((prev) => ({
...prev,
role: [event.target.value],
}))
}
items={[
{ _id: "admin", name: "Admin" },
{ _id: "user", name: "User" },
]}
/>
<Stack
direction="row"
gap={theme.spacing(4)}
mt={theme.spacing(8)}
justifyContent="flex-end"
<LoadingButton
loading={isSendingInvite}
variant="text"
color="info"
onClick={closeInviteModal}
>
<LoadingButton
loading={isSendingInvite}
variant="text"
color="info"
onClick={closeInviteModal}
>
Cancel
</LoadingButton>
<LoadingButton
variant="contained"
color="primary"
onClick={handleInviteMember}
loading={isSendingInvite}
disabled={isDisabled}
>
Send invite
</LoadingButton>
</Stack>
Cancel
</LoadingButton>
<LoadingButton
variant="contained"
color="primary"
onClick={handleInviteMember}
loading={isSendingInvite}
disabled={isDisabled}
>
Send invite
</LoadingButton>
</Stack>
</Modal>
</GenericDialog>
</TabPanel>
);
};
@@ -49,6 +49,7 @@ export const updateAppSettings = createAsyncThunk(
systemEmailAddress: settings.systemEmailAddress,
systemEmailPassword: settings.systemEmailPassword,
};
console.log(parsedSettings);
const res = await networkService.updateAppSettings({
settings: parsedSettings,
authToken,
@@ -0,0 +1,32 @@
import { useTheme } from "@emotion/react";
import PlaceholderLight from "../../../../assets/Images/data_placeholder.svg?react";
import PlaceholderDark from "../../../../assets/Images/data_placeholder_dark.svg?react";
import { Box, Typography } from "@mui/material";
import PropTypes from "prop-types";
const Empty = ({ styles, mode }) => {
const theme = useTheme();
return (
<Box sx={{ ...styles }}>
<Box
textAlign="center"
pb={theme.spacing(20)}
>
{mode === "light" ? <PlaceholderLight /> : <PlaceholderDark />}
</Box>
<Typography
textAlign="center"
color={theme.palette.text.secondary}
>
No incidents recorded yet.
</Typography>
</Box>
);
};
Empty.propTypes = {
styles: PropTypes.object,
mode: PropTypes.string,
};
export { Empty };
@@ -0,0 +1,21 @@
import { Skeleton /* , Stack */ } from "@mui/material";
const IncidentSkeleton = () => {
return (
<>
<Skeleton
animation={"wave"}
variant="rounded"
width="100%"
height={300}
/>
<Skeleton
animation={"wave"}
variant="rounded"
width="100%"
height={100}
/>
</>
);
};
export { IncidentSkeleton };
@@ -25,6 +25,8 @@ import { formatDateWithTz } from "../../../Utils/timeUtils";
import PlaceholderLight from "../../../assets/Images/data_placeholder.svg?react";
import PlaceholderDark from "../../../assets/Images/data_placeholder_dark.svg?react";
import { HttpStatusLabel } from "../../../Components/HttpStatusLabel";
import { Empty } from "./Empty/Empty";
import { IncidentSkeleton } from "./Skeleton/Skeleton";
const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
const uiTimezone = useSelector((state) => state.ui.timezone);
@@ -38,6 +40,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
page: 0,
rowsPerPage: 14,
});
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setPaginationController((prevPaginationController) => ({
@@ -52,6 +55,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
return;
}
try {
setIsLoading(true);
let res;
if (selectedMonitor === "0") {
res = await networkService.getChecksByTeam({
@@ -80,6 +84,8 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
setChecksCount(res.data.data.checksCount);
} catch (error) {
logger.error(error);
} finally {
setIsLoading(false);
}
};
fetchPage();
@@ -130,24 +136,20 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
p: theme.spacing(30),
};
const hasChecks = checks?.length === 0;
const noIncidentsRecordedYet = hasChecks && selectedMonitor === "0";
const noIncidentsForThatMonitor = hasChecks && selectedMonitor !== "0";
return (
<>
{checks?.length === 0 && selectedMonitor === "0" ? (
<Box sx={{ ...sharedStyles }}>
<Box
textAlign="center"
pb={theme.spacing(20)}
>
{mode === "light" ? <PlaceholderLight /> : <PlaceholderDark />}
</Box>
<Typography
textAlign="center"
color={theme.palette.text.secondary}
>
No incidents recorded yet.
</Typography>
</Box>
) : checks?.length === 0 ? (
{isLoading ? (
<IncidentSkeleton />
) : noIncidentsRecordedYet ? (
<Empty
mode={mode}
styles={sharedStyles}
/>
) : noIncidentsForThatMonitor ? (
<Box sx={{ ...sharedStyles }}>
<Box
textAlign="center"
+35 -29
View File
@@ -17,56 +17,62 @@ const Incidents = () => {
const [monitors, setMonitors] = useState({});
const [selectedMonitor, setSelectedMonitor] = useState("0");
const [loading, setLoading] = useState(false);
const [isLoading, setIsLoading] = useState(true);
// TODO do something with these filters
const [filter, setFilter] = useState("all");
useEffect(() => {
const fetchMonitors = async () => {
setLoading(true);
const res = await networkService.getMonitorsByTeamId({
authToken: authState.authToken,
teamId: authState.user.teamId,
limit: -1,
types: null,
status: null,
checkOrder: null,
normalize: null,
page: null,
rowsPerPage: null,
filter: null,
field: null,
order: null,
});
// Reduce to a lookup object for 0(1) lookup
if (res?.data?.data?.monitors?.length > 0) {
const monitorLookup = res.data.data.monitors.reduce((acc, monitor) => {
acc[monitor._id] = monitor;
return acc;
}, {});
setMonitors(monitorLookup);
monitorId !== undefined && setSelectedMonitor(monitorId);
try {
setIsLoading(true);
const res = await networkService.getMonitorsByTeamId({
authToken: authState.authToken,
teamId: authState.user.teamId,
limit: -1,
types: null,
status: null,
checkOrder: null,
normalize: null,
page: null,
rowsPerPage: null,
filter: null,
field: null,
order: null,
});
// Reduce to a lookup object for 0(1) lookup
if (res?.data?.data?.monitors?.length > 0) {
const monitorLookup = res.data.data.monitors.reduce((acc, monitor) => {
acc[monitor._id] = monitor;
return acc;
}, {});
setMonitors(monitorLookup);
monitorId !== undefined && setSelectedMonitor(monitorId);
}
} catch (error) {
console.info(error);
} finally {
setIsLoading(false);
}
setLoading(false);
};
fetchMonitors();
}, [authState]);
useEffect(() => {}, []);
useEffect(() => {}, [monitors]);
const handleSelect = (event) => {
setSelectedMonitor(event.target.value);
};
const isActuallyLoading = isLoading && Object.keys(monitors)?.length === 0;
return (
<Stack
className="incidents table-container"
className="incidents"
pt={theme.spacing(6)}
gap={theme.spacing(12)}
>
{loading ? (
{isActuallyLoading ? (
<SkeletonLayout />
) : (
<>
@@ -2,23 +2,16 @@ import { useState } from "react";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import {
Button,
IconButton,
Menu,
MenuItem,
Modal,
Stack,
Typography,
} from "@mui/material";
import LoadingButton from "@mui/lab/LoadingButton";
import { IconButton, Menu, MenuItem } from "@mui/material";
import { logger } from "../../../../Utils/Logger";
import Settings from "../../../../assets/icons/settings-bold.svg?react";
import PropTypes from "prop-types";
import { networkService } from "../../../../main";
import { createToast } from "../../../../Utils/toastUtils";
const ActionsMenu = ({ isAdmin, maintenanceWindow, updateCallback }) => {
import Dialog from "../../../../Components/Dialog";
const ActionsMenu = ({ /* isAdmin, */ maintenanceWindow, updateCallback }) => {
maintenanceWindow;
const { authToken } = useSelector((state) => state.auth);
const [anchorEl, setAnchorEl] = useState(null);
@@ -155,71 +148,21 @@ const ActionsMenu = ({ isAdmin, maintenanceWindow, updateCallback }) => {
Remove
</MenuItem>
</Menu>
<Modal
aria-labelledby="modal-delete-monitor"
aria-describedby="delete-monitor-confirmation"
<Dialog
open={isOpen}
onClose={(e) => {
theme={theme}
title={"Do you really want to remove this maintenance window?"}
onCancel={(e) => {
e.stopPropagation();
setIsOpen(false);
}}
>
<Stack
gap={theme.spacing(2)}
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 400,
bgcolor: theme.palette.background.main,
border: 1,
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
boxShadow: 24,
p: theme.spacing(15),
"&:focus": {
outline: "none",
},
}}
>
<Typography
id="modal-delete-maintenance"
component="h2"
variant="h2"
>
Do you really want to remove this maintenance window?
</Typography>
<Stack
direction="row"
gap={theme.spacing(4)}
mt={theme.spacing(12)}
justifyContent="flex-end"
>
<Button
variant="text"
color="info"
onClick={(e) => {
e.stopPropagation();
setIsOpen(false);
}}
>
Cancel
</Button>
<LoadingButton
loading={isLoading}
variant="contained"
color="error"
onClick={(e) => {
e.stopPropagation(e);
handleRemove(e);
}}
>
Delete
</LoadingButton>
</Stack>
</Stack>
</Modal>
confirmationButtonLabel={"Delete"}
onConfirm={(e) => {
e.stopPropagation(e);
handleRemove(e);
}}
isLoading={isLoading}
/>
</>
);
};
+7 -10
View File
@@ -453,20 +453,17 @@ const Configure = () => {
</Stack>
</>
)}
<Dialog
modelTitle="modal-delete-monitor"
modelDescription="delete-monitor-confirmation"
open={isOpen}
onClose={() => setIsOpen(false)}
title="Do you really want to delete this monitor?"
confirmationBtnLbl="Delete"
confirmationBtnOnClick={handleRemove}
cancelBtnLbl="Cancel"
cancelBtnOnClick={() => setIsOpen(false)}
theme={theme}
isLoading={isLoading}
title="Do you really want to delete this monitor?"
description="Once deleted, this monitor cannot be retrieved."
></Dialog>
onCancel={() => setIsOpen(false)}
confirmationButtonLabel="Delete"
onConfirm={handleRemove}
isLoading={isLoading}
/>
</Stack>
);
};
@@ -0,0 +1,79 @@
import { useTheme } from "@emotion/react";
import { Box, Stack, Typography } from "@mui/material";
import Search from "../../../../Components/Inputs/Search";
import MemoizedMonitorTable from "../MonitorTable";
import { useState } from "react";
import useDebounce from "../../../../Utils/debounce";
import PropTypes from "prop-types";
const CurrentMonitoring = ({ totalMonitors, monitors, isAdmin }) => {
const theme = useTheme();
const [search, setSearch] = useState("");
const [isSearching, setIsSearching] = useState(false);
const debouncedFilter = useDebounce(search, 500);
const handleSearch = (value) => {
setIsSearching(true);
setSearch(value);
};
return (
<Box
flex={1}
px={theme.spacing(10)}
py={theme.spacing(8)}
border={1}
borderColor={theme.palette.border.light}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.background.main}
>
<Stack
direction="row"
alignItems="center"
mb={theme.spacing(8)}
>
<Typography
component="h2"
variant="h2"
fontWeight={500}
letterSpacing={-0.2}
>
Actively monitoring
</Typography>
<Box
className="current-monitors-counter"
color={theme.palette.text.primary}
border={1}
borderColor={theme.palette.border.light}
backgroundColor={theme.palette.background.accent}
>
{totalMonitors}
</Box>
<Box
width="25%"
minWidth={150}
ml="auto"
>
<Search
options={monitors}
filteredBy="name"
inputValue={search}
handleInputChange={handleSearch}
/>
</Box>
</Stack>
<MemoizedMonitorTable
isAdmin={isAdmin}
filter={debouncedFilter}
setIsSearching={setIsSearching}
isSearching={isSearching}
/>
</Box>
);
};
CurrentMonitoring.propTypes = {
totalMonitors: PropTypes.number,
monitors: PropTypes.array,
isAdmin: PropTypes.bool,
};
export { CurrentMonitoring };
@@ -0,0 +1,31 @@
import { Skeleton, TableCell, TableRow } from "@mui/material";
const ROWS_NUMBER = 7;
const ROWS_ARRAY = Array.from({ length: ROWS_NUMBER }, (_, i) => i);
const TableBodySkeleton = () => {
return (
<>
{ROWS_ARRAY.map((row) => (
<TableRow key={row}>
<TableCell>
<Skeleton />
</TableCell>
<TableCell>
<Skeleton />
</TableCell>
<TableCell>
<Skeleton />
</TableCell>
<TableCell>
<Skeleton />
</TableCell>
<TableCell>
<Skeleton />
</TableCell>
</TableRow>
))}
</>
);
};
export { TableBodySkeleton };
@@ -12,6 +12,7 @@ import {
Stack,
Typography,
Button,
CircularProgress,
} from "@mui/material";
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded";
@@ -34,6 +35,7 @@ import RightArrow from "../../../../assets/icons/right-arrow.svg?react";
import SelectorVertical from "../../../../assets/icons/selector-vertical.svg?react";
import ActionsMenu from "../actionsMenu";
import useUtils from "../../utils";
import { TableBodySkeleton } from "./Skeleton";
/**
* Component for pagination actions (first, previous, next, last).
@@ -107,17 +109,17 @@ TablePaginationActions.propTypes = {
onPageChange: PropTypes.func.isRequired,
};
const MonitorTable = ({ isAdmin, filter, setLoading }) => {
const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => {
const theme = useTheme();
const navigate = useNavigate();
const dispatch = useDispatch();
const { determineState } = useUtils();
const { rowsPerPage } = useSelector((state) => state.ui.monitors);
const authState = useSelector((state) => state.auth);
const [page, setPage] = useState(0);
const [monitors, setMonitors] = useState([]);
const [monitorCount, setMonitorCount] = useState(0);
const authState = useSelector((state) => state.auth);
const [updateTrigger, setUpdateTrigger] = useState(false);
const [sort, setSort] = useState({});
const prevFilter = useRef(filter);
@@ -160,15 +162,25 @@ const MonitorTable = ({ isAdmin, filter, setLoading }) => {
});
setMonitors(res?.data?.data?.monitors ?? []);
setMonitorCount(res?.data?.data?.monitorCount ?? 0);
setLoading(false);
} catch (error) {
logger.error(error);
} finally {
setIsSearching(false);
}
}, [authState, page, rowsPerPage, filter, sort, setLoading]);
}, [authState, page, rowsPerPage, filter, sort, setIsSearching]);
useEffect(() => {
fetchPage();
}, [updateTrigger, authState, page, rowsPerPage, filter, sort, setLoading, fetchPage]);
}, [
updateTrigger,
authState,
page,
rowsPerPage,
filter,
sort,
setIsSearching,
fetchPage,
]);
// Listen for changes in filter, if new value reset the page
useEffect(() => {
@@ -220,7 +232,37 @@ const MonitorTable = ({ isAdmin, filter, setLoading }) => {
};
return (
<>
<Box position="relative">
{isSearching && (
<>
<Box
width="100%"
height="100%"
position="absolute"
sx={{
backgroundColor: theme.palette.background.main,
opacity: 0.8,
zIndex: 100,
}}
/>
<Box
height="100%"
position="absolute"
top="20%"
left="50%"
sx={{
transform: "translateX(-50%)",
zIndex: 101,
}}
>
<CircularProgress
sx={{
color: theme.palette.other.icon,
}}
/>
</Box>
</>
)}
<TableContainer component={Paper}>
<Table>
<TableHead>
@@ -271,77 +313,82 @@ const MonitorTable = ({ isAdmin, filter, setLoading }) => {
</TableRow>
</TableHead>
<TableBody>
{monitors.map((monitor) => {
let uptimePercentage = "";
let percentageColor = theme.palette.percentage.uptimeExcellent;
{/* TODO add empty state. Check if is searching, and empty => skeleton. Is empty, not searching => skeleton */}
{monitors.length > 0 ? (
monitors.map((monitor) => {
let uptimePercentage = "";
let percentageColor = theme.palette.percentage.uptimeExcellent;
// Determine uptime percentage and color based on the monitor's uptimePercentage value
if (monitor.uptimePercentage !== undefined) {
uptimePercentage =
monitor.uptimePercentage === 0
? "0"
: (monitor.uptimePercentage * 100).toFixed(2);
// Determine uptime percentage and color based on the monitor's uptimePercentage value
if (monitor.uptimePercentage !== undefined) {
uptimePercentage =
monitor.uptimePercentage === 0
? "0"
: (monitor.uptimePercentage * 100).toFixed(2);
percentageColor =
monitor.uptimePercentage < 0.25
? theme.palette.percentage.uptimePoor
: monitor.uptimePercentage < 0.5
? theme.palette.percentage.uptimeFair
: monitor.uptimePercentage < 0.75
? theme.palette.percentage.uptimeGood
: theme.palette.percentage.uptimeExcellent;
}
percentageColor =
monitor.uptimePercentage < 0.25
? theme.palette.percentage.uptimePoor
: monitor.uptimePercentage < 0.5
? theme.palette.percentage.uptimeFair
: monitor.uptimePercentage < 0.75
? theme.palette.percentage.uptimeGood
: theme.palette.percentage.uptimeExcellent;
}
const params = {
url: monitor.url,
title: monitor.name,
percentage: uptimePercentage,
percentageColor,
status: determineState(monitor),
};
const params = {
url: monitor.url,
title: monitor.name,
percentage: uptimePercentage,
percentageColor,
status: determineState(monitor),
};
return (
<TableRow
key={monitor._id}
sx={{
cursor: "pointer",
"&:hover": {
backgroundColor: theme.palette.background.accent,
},
}}
onClick={() => {
navigate(`/monitors/${monitor._id}`);
}}
>
<TableCell>
<Host
key={monitor._id}
params={params}
/>
</TableCell>
<TableCell>
<StatusLabel
status={params.status}
text={params.status}
customStyles={{ textTransform: "capitalize" }}
/>
</TableCell>
<TableCell>
<BarChart checks={monitor.checks.slice().reverse()} />
</TableCell>
<TableCell>
<span style={{ textTransform: "uppercase" }}>{monitor.type}</span>
</TableCell>
<TableCell>
<ActionsMenu
monitor={monitor}
isAdmin={isAdmin}
updateCallback={handleActionMenuDelete}
/>
</TableCell>
</TableRow>
);
})}
return (
<TableRow
key={monitor._id}
sx={{
cursor: "pointer",
"&:hover": {
backgroundColor: theme.palette.background.accent,
},
}}
onClick={() => {
navigate(`/monitors/${monitor._id}`);
}}
>
<TableCell>
<Host
key={monitor._id}
params={params}
/>
</TableCell>
<TableCell>
<StatusLabel
status={params.status}
text={params.status}
customStyles={{ textTransform: "capitalize" }}
/>
</TableCell>
<TableCell>
<BarChart checks={monitor.checks.slice().reverse()} />
</TableCell>
<TableCell>
<span style={{ textTransform: "uppercase" }}>{monitor.type}</span>
</TableCell>
<TableCell>
<ActionsMenu
monitor={monitor}
isAdmin={isAdmin}
updateCallback={handleActionMenuDelete}
/>
</TableCell>
</TableRow>
);
})
) : (
<TableBodySkeleton />
)}
</TableBody>
</Table>
</TableContainer>
@@ -415,14 +462,15 @@ const MonitorTable = ({ isAdmin, filter, setLoading }) => {
}}
/>
</Stack>
</>
</Box>
);
};
MonitorTable.propTypes = {
isAdmin: PropTypes.bool,
filter: PropTypes.string,
setLoading: PropTypes.func,
setIsSearching: PropTypes.func,
isSearching: PropTypes.bool,
};
const MemoizedMonitorTable = memo(MonitorTable);
+13 -13
View File
@@ -188,25 +188,25 @@ const ActionsMenu = ({ monitor, isAdmin, updateCallback }) => {
)}
</Menu>
<Dialog
modelTitle="modal-delete-monitor"
modelDescription="delete-monitor-confirmation"
open={isOpen}
onClose={() => setIsOpen(false)}
theme={theme}
title="Do you really want to delete this monitor?"
confirmationBtnLbl="Delete"
confirmationBtnOnClick={(e) => {
e.stopPropagation();
handleRemove(e);
}}
cancelBtnLbl="Cancel"
cancelBtnOnClick={(e) => {
description="Once deleted, this monitor cannot be retrieved."
/* Do we need stop propagation? */
onCancel={(e) => {
e.stopPropagation();
setIsOpen(false);
}}
theme={theme}
confirmationButtonLabel="Delete"
/* Do we need stop propagation? */
onConfirm={(e) => {
e.stopPropagation();
handleRemove(e);
}}
isLoading={isLoading}
description="Once deleted, this monitor cannot be retrieved."
></Dialog>
modelTitle="modal-delete-monitor"
modelDescription="delete-monitor-confirmation"
/>
</>
);
};
+44 -126
View File
@@ -1,79 +1,74 @@
import "./index.css";
import { useEffect, useState } from "react";
import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { getUptimeMonitorsByTeamId } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
import { useNavigate } from "react-router-dom";
import { useTheme } from "@emotion/react";
import { Box, Button, CircularProgress, Stack, Typography } from "@mui/material";
import { Box, Button, Stack } from "@mui/material";
import PropTypes from "prop-types";
import SkeletonLayout from "./skeleton";
import Fallback from "./fallback";
import StatusBox from "./StatusBox";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import Greeting from "../../../Utils/greeting";
import MonitorTable from "./MonitorTable";
import Search from "../../../Components/Inputs/Search";
import useDebounce from "../../../Utils/debounce";
import { CurrentMonitoring } from "./CurrentMonitoring";
const Monitors = ({ isAdmin }) => {
const theme = useTheme();
const navigate = useNavigate();
const monitorState = useSelector((state) => state.uptimeMonitors);
const authState = useSelector((state) => state.auth);
const [search, setSearch] = useState("");
const [isSearching, setIsSearching] = useState(false);
const dispatch = useDispatch({});
const debouncedFilter = useDebounce(search, 500);
const handleSearch = (value) => {
setIsSearching(true);
setSearch(value);
};
useEffect(() => {
dispatch(getUptimeMonitorsByTeamId(authState.authToken));
}, [authState.authToken, dispatch]);
let loading =
monitorState?.isLoading && monitorState?.monitorsSummary?.monitors?.length === 0;
//TODO bring fetching to this component, like on pageSpeed
const loading = monitorState?.isLoading;
const totalMonitors = monitorState?.monitorsSummary?.monitorCounts?.total;
const hasMonitors = totalMonitors > 0;
const noMonitors = !hasMonitors;
const canAddMonitor = isAdmin && hasMonitors;
return (
<Stack
className="monitors table-container"
className="monitors"
gap={theme.spacing(8)}
>
<Box>
<Breadcrumbs list={[{ name: `monitors`, path: "/monitors" }]} />
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
mt={theme.spacing(5)}
gap={theme.spacing(6)}
>
<Greeting type="uptime" />
{canAddMonitor && (
<Button
variant="contained"
color="primary"
onClick={() => {
navigate("/monitors/create");
}}
sx={{ fontWeight: 500 }}
>
Create monitor
</Button>
)}
</Stack>
</Box>
{noMonitors && <Fallback isAdmin={isAdmin} />}
{loading ? (
<SkeletonLayout />
) : (
<>
<Box>
<Breadcrumbs list={[{ name: `monitors`, path: "/monitors" }]} />
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
mt={theme.spacing(5)}
>
<Greeting type="uptime" />
{isAdmin && monitorState?.monitorsSummary?.monitors?.length !== 0 && (
<Button
variant="contained"
color="primary"
onClick={() => {
navigate("/monitors/create");
}}
sx={{ fontWeight: 500 }}
>
Create monitor
</Button>
)}
</Stack>
</Box>
{isAdmin && monitorState?.monitorsSummary?.monitors?.length === 0 && (
<Fallback isAdmin={isAdmin} />
)}
{monitorState?.monitorsSummary?.monitors?.length !== 0 && (
{hasMonitors && (
<>
<Stack
gap={theme.spacing(8)}
@@ -93,88 +88,11 @@ const Monitors = ({ isAdmin }) => {
value={monitorState?.monitorsSummary?.monitorCounts?.paused ?? 0}
/>
</Stack>
<Box
flex={1}
px={theme.spacing(10)}
py={theme.spacing(8)}
border={1}
borderColor={theme.palette.border.light}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.background.main}
>
<Stack
direction="row"
alignItems="center"
mb={theme.spacing(8)}
>
<Typography
component="h2"
variant="h2"
fontWeight={500}
letterSpacing={-0.2}
>
Actively monitoring
</Typography>
<Box
className="current-monitors-counter"
color={theme.palette.text.primary}
border={1}
borderColor={theme.palette.border.light}
backgroundColor={theme.palette.background.accent}
>
{monitorState?.monitorsSummary?.monitorCounts?.total || 0}
</Box>
<Box
width="25%"
minWidth={150}
ml="auto"
>
<Search
options={monitorState?.monitorsSummary?.monitors ?? []}
filteredBy="name"
inputValue={search}
handleInputChange={handleSearch}
/>
</Box>
</Stack>
<Box position="relative">
{isSearching && (
<>
<Box
width="100%"
height="100%"
position="absolute"
sx={{
backgroundColor: theme.palette.background.main,
opacity: 0.8,
zIndex: 100,
}}
/>
<Box
height="100%"
position="absolute"
top="20%"
left="50%"
sx={{
transform: "translateX(-50%)",
zIndex: 101,
}}
>
<CircularProgress
sx={{
color: theme.palette.other.icon,
}}
/>
</Box>
</>
)}
<MonitorTable
isAdmin={isAdmin}
filter={debouncedFilter}
setLoading={setIsSearching}
/>
</Box>
</Box>
<CurrentMonitoring
isAdmin={isAdmin}
monitors={monitorState.monitorsSummary.monitors}
totalMonitors={totalMonitors}
/>
</>
)}
</>
+14 -63
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { useTheme } from "@emotion/react";
import { Box, Button, Modal, Stack, Tooltip, Typography } from "@mui/material";
import { Box, Stack, Tooltip, Typography } from "@mui/material";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router";
import {
@@ -25,6 +25,7 @@ import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineR
import SkeletonLayout from "./skeleton";
import useUtils from "../../Monitors/utils";
import "./index.css";
import Dialog from "../../../Components/Dialog";
const PageSpeedConfigure = () => {
const theme = useTheme();
@@ -37,6 +38,7 @@ const PageSpeedConfigure = () => {
const [monitor, setMonitor] = useState({});
const [errors, setErrors] = useState({});
const { statusColor, pagespeedStatusMsg, determineState } = useUtils();
const [buttonLoading, setButtonLoading] = useState(false);
const idMap = {
"monitor-url": "url",
"monitor-name": "name",
@@ -156,12 +158,14 @@ const PageSpeedConfigure = () => {
const [isOpen, setIsOpen] = useState(false);
const handleRemove = async (event) => {
event.preventDefault();
setButtonLoading(true);
const action = await dispatch(deletePageSpeed({ authToken, monitor }));
if (action.meta.requestStatus === "fulfilled") {
navigate("/pagespeed");
} else {
createToast({ body: "Failed to delete monitor." });
}
setButtonLoading(false);
};
return (
@@ -428,69 +432,16 @@ const PageSpeedConfigure = () => {
</Stack>
</>
)}
<Modal
aria-labelledby="modal-delete-pagespeed-monitor"
aria-describedby="delete-pagespeed-monitor-confirmation"
<Dialog
open={isOpen}
onClose={() => setIsOpen(false)}
disablePortal
>
<Stack
gap={theme.spacing(2)}
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 400,
bgcolor: theme.palette.background.main,
border: 1,
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
boxShadow: 24,
p: theme.spacing(15),
"&:focus": {
outline: "none",
},
}}
>
<Typography
id="modal-delete-pagespeed-monitor"
component="h2"
variant="h2"
fontWeight={500}
>
Do you really want to delete this monitor?
</Typography>
<Typography
id="delete-pagespeed-monitor-confirmation"
variant="body1"
>
Once deleted, this monitor cannot be retrieved.
</Typography>
<Stack
direction="row"
gap={theme.spacing(4)}
mt={theme.spacing(12)}
justifyContent="flex-end"
>
<Button
variant="text"
color="info"
onClick={() => setIsOpen(false)}
>
Cancel
</Button>
<Button
variant="contained"
color="error"
onClick={handleRemove}
>
Delete
</Button>
</Stack>
</Stack>
</Modal>
theme={theme}
title={"Do you really want to delete this monitor?"}
description={"Once deleted, this monitor cannot be retrieved."}
onCancel={() => setIsOpen(false)}
confirmationButtonLabel={"Delete"}
onConfirm={handleRemove}
isLoading={buttonLoading}
/>
</Stack>
);
};
+1 -1
View File
@@ -19,7 +19,7 @@ const PageSpeed = ({ isAdmin }) => {
const navigate = useNavigate();
const { user, authToken } = useSelector((state) => state.auth);
const [isLoading, setIsLoading] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [monitors, setMonitors] = useState([]);
useEffect(() => {
dispatch(getPageSpeedByTeamId(authToken));
+11 -18
View File
@@ -263,18 +263,15 @@ const Settings = ({ isAdmin }) => {
</Box>
</Stack>
<Dialog
modelTitle="model-clear-stats"
modelDescription="clear-stats-confirmation"
open={isOpen.deleteStats}
onClose={() => setIsOpen(deleteStatsMonitorsInitState)}
title="Do you want to clear all stats?"
confirmationBtnLbl="Yes, clear all stats"
confirmationBtnOnClick={handleClearStats}
cancelBtnLbl="Cancel"
cancelBtnOnClick={() => setIsOpen(deleteStatsMonitorsInitState)}
theme={theme}
title="Do you want to clear all stats?"
description="Once deleted, this monitor cannot be retrieved."
onCancel={() => setIsOpen(deleteStatsMonitorsInitState)}
confirmationButtonLabel="Yes, clear all stats"
onConfirm={handleClearStats}
isLoading={isLoading || authIsLoading || checksIsLoading}
></Dialog>
/>
</ConfigBox>
)}
{isAdmin && (
@@ -314,18 +311,14 @@ const Settings = ({ isAdmin }) => {
</Box>
</Stack>
<Dialog
modelTitle="model-delete-all-monitors"
modelDescription="delete-all-monitors-confirmation"
open={isOpen.deleteMonitors}
onClose={() => setIsOpen(deleteStatsMonitorsInitState)}
title="Do you want to remove all monitors?"
confirmationBtnLbl="Yes, clear all monitors"
confirmationBtnOnClick={handleDeleteAllMonitors}
cancelBtnLbl="Cancel"
cancelBtnOnClick={() => setIsOpen(deleteStatsMonitorsInitState)}
theme={theme}
title="Do you want to remove all monitors?"
onCancel={() => setIsOpen(deleteStatsMonitorsInitState)}
confirmationButtonLabel="Yes, clear all monitors"
onConfirm={handleDeleteAllMonitors}
isLoading={isLoading || authIsLoading || checksIsLoading}
></Dialog>
/>
</ConfigBox>
)}
{isAdmin && (
+41 -40
View File
@@ -13,6 +13,7 @@ class NetworkService {
this.setBaseUrl(baseURL);
this.unsubscribe = store.subscribe(() => {
const state = store.getState();
console.log(state.settings.apiBaseUrl);
if (BASE_URL !== undefined) {
baseURL = BASE_URL;
} else if (state?.settings?.apiBaseUrl ?? null) {
@@ -87,48 +88,48 @@ class NetworkService {
},
});
}
/**
*
* ************************************
* Check the endpoint resolution
* ************************************
*
* @async
* @param {Object} config - The configuration object.
* @param {string} config.authToken - The authorization token to be used in the request header.
* @param {Object} config.monitorURL - The monitor url to be sent in the request body.
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
*/
async checkEndpointResolution(config) {
const { authToken, monitorURL } = config;
const params = new URLSearchParams();
if (monitorURL) params.append("monitorURL", monitorURL);
return this.axiosInstance.get(`/monitors/resolution/url?${params.toString()}`, {
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
}
})
}
/**
*
* ************************************
* Check the endpoint resolution
* ************************************
*
* @async
* @param {Object} config - The configuration object.
* @param {string} config.authToken - The authorization token to be used in the request header.
* @param {Object} config.monitorURL - The monitor url to be sent in the request body.
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
*/
async checkEndpointResolution(config) {
const { authToken, monitorURL } = config;
const params = new URLSearchParams();
/**
*
* ************************************
* Gets monitors and summary of stats by TeamID
* ************************************
*
* @async
* @param {Object} config - The configuration object.
* @param {string} config.authToken - The authorization token to be used in the request header.
* @param {string} config.teamId - Team ID
* @param {Array<string>} config.types - Array of monitor types
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
*/
async getMonitorsAndSummaryByTeamId(config) {
const params = new URLSearchParams();
if (monitorURL) params.append("monitorURL", monitorURL);
return this.axiosInstance.get(`/monitors/resolution/url?${params.toString()}`, {
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
});
}
/**
*
* ************************************
* Gets monitors and summary of stats by TeamID
* ************************************
*
* @async
* @param {Object} config - The configuration object.
* @param {string} config.authToken - The authorization token to be used in the request header.
* @param {string} config.teamId - Team ID
* @param {Array<string>} config.types - Array of monitor types
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
*/
async getMonitorsAndSummaryByTeamId(config) {
const params = new URLSearchParams();
if (config.types) {
config.types.forEach((type) => {
+19 -17
View File
@@ -1,24 +1,26 @@
#!/bin/bash
# Change directory to root Server directory for correct Docker Context
cd "$(dirname "$0")"
cd ../..
#Client
client="./Docker/dist/client.Dockerfile"
# Define an array of services and their Dockerfiles
declare -A services=(
["bluewave/uptime_client"]="./Docker/dist/client.Dockerfile"
["bluewave/database_mongo"]="./Docker/dist/mongoDB.Dockerfile"
["bluewave/uptime_redis"]="./Docker/dist/redis.Dockerfile"
["bluewave/uptime_server"]="./Docker/dist/server.Dockerfile"
)
# MongoDB
mongoDB="./Docker/dist/mongoDB.Dockerfile"
# Loop through each service and build the corresponding image
for service in "${!services[@]}"; do
docker build -f "${services[$service]}" -t "$service" .
# Check if the build succeeded
if [ $? -ne 0 ]; then
echo "Error building $service image. Exiting..."
exit 1
fi
done
# Redis
redis="./Docker/dist/redis.Dockerfile"
# Server
server="./Docker/dist/server.Dockerfile"
docker build -f $client -t dist_uptime_client .
docker build -f $mongoDB -t dist_uptime_database_mongo .
docker build -f $redis -t dist_uptime_redis .
docker build -f $server -t dist_uptime_server .
echo "All images built"
echo "All images built successfully"
+19 -17
View File
@@ -1,24 +1,26 @@
#!/bin/bash
# Change directory to root directory for correct Docker Context
cd "$(dirname "$0")"
cd ../..
#Client
client="./Docker/prod/client.Dockerfile"
# Define an array of services and their Dockerfiles
declare -A services=(
["uptime_client"]="./Docker/prod/client.Dockerfile"
["uptime_database_mongo"]="./Docker/prod/mongoDB.Dockerfile"
["uptime_redis"]="./Docker/prod/redis.Dockerfile"
["uptime_server"]="./Docker/prod/server.Dockerfile"
)
# MongoDB
mongoDB="./Docker/prod/mongoDB.Dockerfile"
# Loop through each service and build the corresponding image
for service in "${!services[@]}"; do
docker build -f "${services[$service]}" -t "$service" .
# Check if the build succeeded
if [ $? -ne 0 ]; then
echo "Error building $service image. Exiting..."
exit 1
fi
done
# Redis
redis="./Docker/prod/redis.Dockerfile"
# Server
server="./Docker/prod/server.Dockerfile"
docker build -f $client -t uptime_client .
docker build -f $mongoDB -t uptime_database_mongo .
docker build -f $redis -t uptime_redis .
docker build -f $server -t uptime_server .
echo "All images built"
echo "All images built successfully"
-15
View File
@@ -1,15 +0,0 @@
const PORT = 5000;
const connectDbAndRunServer = async (app, db) => {
try {
await db.connect();
app.listen(PORT, () => {
console.log(`server started on port:${PORT}`);
});
} catch (error) {
console.log("Failed to connect to DB");
console.error(error);
}
};
export { connectDbAndRunServer };
+87 -37
View File
@@ -79,10 +79,10 @@ const registerUser = async (req, res, next) => {
}
const newUser = await req.db.insertUser({ ...req.body }, req.file);
logger.info(successMessages.AUTH_CREATE_USER, {
logger.info({
message: successMessages.AUTH_CREATE_USER,
service: SERVICE_NAME,
userId: newUser._id,
details: newUser._id,
});
const userForToken = { ...newUser._doc };
@@ -102,9 +102,11 @@ const registerUser = async (req, res, next) => {
"Welcome to Uptime Monitor"
)
.catch((error) => {
logger.error("Error sending welcome email", {
logger.error({
message: error.message,
service: SERVICE_NAME,
error: error.message,
method: "registerUser",
stack: error.stack,
});
});
@@ -173,6 +175,65 @@ const loginUser = async (req, res, next) => {
}
};
/**
* Generates new auth token if the refresh token is valid
* @async
* @param {Express.Request} req - The Express request object.
* @property {Object} req.headers - The parameter of the request.
* @param {Express.Response} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message indicating new auth token is generated.
* @throws {Error} If there is an error during the process such as any of the token is not received
*/
const refreshAuthToken = async (req, res, next) => {
try {
// check for refreshToken
const refreshToken = req.headers["x-refresh-token"];
if (!refreshToken) {
// No refresh token provided
const error = new Error(errorMessages.NO_REFRESH_TOKEN);
error.status = 401;
error.service = SERVICE_NAME;
error.method = "refreshAuthToken";
return next(error);
}
// Verify refresh token
const appSettings = await req.settingsService.getSettings();
const { refreshTokenSecret } = appSettings;
jwt.verify(refreshToken, refreshTokenSecret, async (refreshErr, refreshDecoded) => {
if (refreshErr) {
// Invalid or expired refresh token, trigger logout
const errorMessage =
refreshErr.name === "TokenExpiredError"
? errorMessages.EXPIRED_REFRESH_TOKEN
: errorMessages.INVALID_REFRESH_TOKEN;
const error = new Error(errorMessage);
error.status = 401;
error.service = SERVICE_NAME;
return next(error);
}
});
// Refresh token is valid and unexpired, generate new access token
const oldAuthToken = getTokenFromHeaders(req.headers);
const { jwtSecret } = await req.settingsService.getSettings();
const payloadData = jwt.verify(oldAuthToken, jwtSecret, { ignoreExpiration: true });
// delete old token related data
delete payloadData.iat;
delete payloadData.exp;
const newAuthToken = issueToken(payloadData, tokenType.ACCESS_TOKEN, appSettings);
return res.status(200).json({
success: true,
msg: successMessages.AUTH_TOKEN_REFRESHED,
data: { user: payloadData, token: newAuthToken, refreshToken: refreshToken },
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "refreshAuthToken"));
}
};
/**
* Edits a user's information. If the user wants to change their password, the current password is checked before updating to the new password.
* @async
@@ -220,10 +281,10 @@ const editUser = async (req, res, next) => {
const user = await req.db.getUserByEmail(email);
// Compare passwords
const match = await user.comparePassword(req.body.password);
// If not a match, throw a 403
// If not a match, throw a 401
if (!match) {
const error = new Error(errorMessages.AUTH_INCORRECT_PASSWORD);
error.status = 403;
error.status = 401;
next(error);
return;
}
@@ -287,32 +348,26 @@ const requestRecovery = async (req, res, next) => {
try {
const { email } = req.body;
const user = await req.db.getUserByEmail(email);
if (user) {
const recoveryToken = await req.db.requestRecoveryToken(req, res);
const name = user.firstName;
const email = req.body.email;
const { clientHost } = req.settingsService.getSettings();
const url = `${clientHost}/set-new-password/${recoveryToken.token}`;
const msgId = await req.emailService.buildAndSendEmail(
"passwordResetTemplate",
{
name,
email,
url,
},
const recoveryToken = await req.db.requestRecoveryToken(req, res);
const name = user.firstName;
const { clientHost } = req.settingsService.getSettings();
const url = `${clientHost}/set-new-password/${recoveryToken.token}`;
const msgId = await req.emailService.buildAndSendEmail(
"passwordResetTemplate",
{
name,
email,
"Bluewave Uptime Password Reset"
);
url,
},
email,
"Bluewave Uptime Password Reset"
);
return res.status(200).json({
success: true,
msg: successMessages.AUTH_CREATE_RECOVERY_TOKEN,
data: msgId,
});
} else {
throw new Error(errorMessages.FRIENDLY_ERROR);
}
return res.status(200).json({
success: true,
msg: successMessages.AUTH_CREATE_RECOVERY_TOKEN,
data: msgId,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "recoveryRequestController"));
}
@@ -371,7 +426,6 @@ const resetPassword = async (req, res, next) => {
}
try {
const user = await req.db.resetPassword(req, res);
const appSettings = await req.settingsService.getSettings();
const token = issueToken(user._doc, tokenType.ACCESS_TOKEN, appSettings);
res.status(200).json({
@@ -401,11 +455,6 @@ const deleteUser = async (req, res, next) => {
// Check if the user exists
const user = await req.db.getUserByEmail(email);
if (!user) {
next(new Error(errorMessages.DB_USER_NOT_FOUND));
return;
}
// 1. Find all the monitors associated with the team ID if superadmin
const result = await req.db.getMonitorsByTeamId({
@@ -455,6 +504,7 @@ export {
issueToken,
registerUser,
loginUser,
refreshAuthToken,
editUser,
checkSuperadminExists,
requestRecovery,
+4 -2
View File
@@ -53,9 +53,11 @@ const issueInvitation = async (req, res, next) => {
"Welcome to Uptime Monitor"
)
.catch((error) => {
logger.error("Error sending invite email", {
logger.error({
message: error.message,
service: SERVICE_NAME,
error: error.message,
method: "issueInvitation",
stack: error.stack,
});
});
+34 -36
View File
@@ -3,7 +3,7 @@ import {
getMonitorByIdQueryValidation,
getMonitorsByTeamIdValidation,
createMonitorBodyValidation,
getMonitorURLByQueryValidation,
getMonitorURLByQueryValidation,
editMonitorBodyValidation,
getMonitorsAndSummaryByTeamIdParamValidation,
getMonitorsAndSummaryByTeamIdQueryValidation,
@@ -270,32 +270,32 @@ const createMonitor = async (req, res, next) => {
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
const checkEndpointResolution = async (req, res, next) => {
try {
try {
await getMonitorURLByQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
let { monitorURL } = req.query;
monitorURL = new URL(monitorURL);
await new Promise((resolve, reject) => {
dns.resolve(monitorURL.hostname, (error) => {
if (error) {
reject(error);
}
resolve();
});
});
return res.status(200).json({
success: true,
msg: `URL resolved successfully`,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "checkEndpointResolution"));
}
}
try {
let { monitorURL } = req.query;
monitorURL = new URL(monitorURL);
await new Promise((resolve, reject) => {
dns.resolve(monitorURL.hostname, (error) => {
if (error) {
reject(error);
}
resolve();
});
});
return res.status(200).json({
success: true,
msg: `URL resolved successfully`,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "checkEndpointResolution"));
}
};
/**
* Deletes a monitor by its ID and also deletes associated checks, alerts, and notifications.
@@ -325,14 +325,12 @@ const deleteMonitor = async (req, res, next) => {
await req.db.deletePageSpeedChecksByMonitorId(monitor._id);
await req.db.deleteNotificationsByMonitorId(monitor._id);
} catch (error) {
logger.error(
`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`,
{
method: "deleteMonitor",
service: SERVICE_NAME,
error: error.message,
}
);
logger.error({
message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`,
service: SERVICE_NAME,
method: "deleteMonitor",
stack: error.stack,
});
}
return res.status(200).json({ success: true, msg: successMessages.MONITOR_DELETE });
} catch (error) {
@@ -365,12 +363,12 @@ const deleteAllMonitors = async (req, res, next) => {
await req.db.deletePageSpeedChecksByMonitorId(monitor._id);
await req.db.deleteNotificationsByMonitorId(monitor._id);
} catch (error) {
logger.error(
`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`,
{
method: "deleteAllMonitors",
}
);
logger.error({
message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`,
service: SERVICE_NAME,
method: "deleteAllMonitors",
stack: error.stack,
});
}
})
);
@@ -517,7 +515,7 @@ export {
getMonitorsAndSummaryByTeamId,
getMonitorsByTeamId,
createMonitor,
checkEndpointResolution,
checkEndpointResolution,
deleteMonitor,
deleteAllMonitors,
editMonitor,
+60
View File
@@ -0,0 +1,60 @@
import mongoose from "mongoose";
const cpuSchema = mongoose.Schema({
physical_core: { type: Number, default: 0 },
logical_core: { type: Number, default: 0 },
frequency: { type: Number, default: 0 },
temperature: { type: Number, default: 0 },
free_percent: { type: Number, default: 0 },
usage_percent: { type: Number, default: 0 },
});
const memorySchema = mongoose.Schema({
total_bytes: { type: Number, default: 0 },
available_bytes: { type: Number, default: 0 },
used_bytes: { type: Number, default: 0 },
usage_percent: { type: Number, default: 0 },
});
const discSchema = mongoose.Schema({
read_speed_bytes: { type: Number, default: 0 },
write_speed_bytes: { type: Number, default: 0 },
total_bytes: { type: Number, default: 0 },
free_bytes: { type: Number, default: 0 },
usage_percent: { type: Number, default: 0 },
});
const hostSchema = mongoose.Schema({
os: { type: String, default: "" },
platform: { type: String, default: "" },
kernel_version: { type: String, default: "" },
});
const HardwareCheckSchema = mongoose.Schema(
{
monitorId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Monitor",
immutable: true,
},
cpu: {
type: cpuSchema,
default: () => ({}),
},
memory: {
type: memorySchema,
default: () => ({}),
},
disk: {
type: [discSchema],
default: () => [],
},
host: {
type: hostSchema,
default: () => ({}),
},
},
{ timestamps: true }
);
export default mongoose.model("HardwareCheck", HardwareCheckSchema);
+1 -1
View File
@@ -29,7 +29,7 @@ const MonitorSchema = mongoose.Schema(
type: {
type: String,
required: true,
enum: ["http", "ping", "pagespeed"],
enum: ["http", "ping", "pagespeed", "hardware"],
},
url: {
type: String,
+9 -5
View File
@@ -1,4 +1,5 @@
import mongoose from "mongoose";
import logger from "../../utils/logger.js";
const AuditSchema = mongoose.Schema({
id: { type: String, required: true },
title: { type: String, required: true },
@@ -90,19 +91,22 @@ PageSpeedCheck.pre("save", async function (next) {
const monitor = await mongoose.model("Monitor").findById(this.monitorId);
if (monitor && monitor.status !== this.status) {
if (monitor.status === true && this.status === false) {
// TODO issue alert
console.log("Monitor went down");
logger.info({ message: "Monitor went down", monitorId: monitor._id });
}
if (monitor.status === false && this.status === true) {
// TODO issue alert
console.log("Monitor went up");
logger.info({ message: "Monitor went up", monitorId: monitor._id });
}
monitor.status = this.status;
await monitor.save();
}
} catch (error) {
console.log(error);
logger.error({
message: error.message,
service: "PageSpeedCheck",
method: "pre-save",
stack: error.stack,
});
} finally {
next();
}
+4 -1
View File
@@ -1,5 +1,6 @@
import mongoose from "mongoose";
import bcrypt from "bcrypt";
import logger from "../../utils/logger.js";
const UserSchema = mongoose.Schema(
{
@@ -81,7 +82,9 @@ UserSchema.methods.comparePassword = async function (submittedPassword) {
const User = mongoose.model("User", UserSchema);
User.init().then(() => {
console.log("User model initialized");
logger.info({
message: "User model initialized",
});
});
export default mongoose.model("User", UserSchema);
+32 -3
View File
@@ -1,6 +1,8 @@
import mongoose from "mongoose";
import UserModel from "../models/User.js";
import AppSettings from "../models/AppSettings.js";
import logger from "../../utils/logger.js";
const SERVICE_NAME = "MongoDB";
//****************************************
// DB Connection
@@ -17,14 +19,34 @@ const connect = async () => {
appSettings = new AppSettings({});
await appSettings.save();
}
console.log("Connected to MongoDB");
logger.info({ message: "Connected to MongoDB" });
} catch (error) {
console.error("Failed to connect to MongoDB");
logger.error({
message: error.message,
service: SERVICE_NAME,
method: "connect",
stack: error.stack,
});
throw error;
}
};
const disconnect = async () => {
try {
logger.info({ message: "Disconnecting from MongoDB" });
await mongoose.disconnect();
logger.info({ message: "Disconnected from MongoDB" });
return;
} catch (error) {
logger.error({
message: error.message,
service: SERVICE_NAME,
method: "disconnect",
stack: error.stack,
});
}
};
const checkSuperadmin = async (req, res) => {
try {
const superAdmin = await UserModel.findOne({ role: "superadmin" });
@@ -99,6 +121,11 @@ import {
deletePageSpeedChecksByMonitorId,
} from "./modules/pageSpeedCheckModule.js";
//****************************************
// Hardware Checks
//****************************************
import { createHardwareCheck } from "./modules/hardwareCheckModule.js";
//****************************************
// Checks
//****************************************
@@ -143,6 +170,7 @@ import { getAppSettings, updateAppSettings } from "./modules/settingsModule.js";
export default {
connect,
disconnect,
insertUser,
getUserByEmail,
updateUser,
@@ -179,6 +207,7 @@ export default {
createPageSpeedCheck,
getPageSpeedChecks,
deletePageSpeedChecksByMonitorId,
createHardwareCheck,
createMaintenanceWindow,
getMaintenanceWindowsByTeamId,
getMaintenanceWindowById,
+23 -7
View File
@@ -30,9 +30,11 @@ const createCheck = async (checkData) => {
const monitor = await Monitor.findById(monitorId);
if (!monitor) {
logger.error("Monitor not found", {
logger.error({
message: "Monitor not found",
service: SERVICE_NAME,
monitorId,
method: "createCheck",
details: `monitor ID: ${monitorId}`,
});
return;
}
@@ -77,7 +79,11 @@ const getChecksCount = async (req) => {
checksQuery.statusCode = 5000;
break;
default:
console.log("default");
logger.warn({
message: "invalid filter",
service: SERVICE_NAME,
method: "getChecksCount",
});
break;
}
}
@@ -110,7 +116,7 @@ const getChecks = async (req) => {
if (dateRange !== undefined) {
checksQuery.createdAt = { $gte: dateRangeLookup[dateRange] };
}
// Fitler checks by status
// Filter checks by status
if (filter !== undefined) {
checksQuery.status = false;
switch (filter) {
@@ -122,7 +128,11 @@ const getChecks = async (req) => {
checksQuery.statusCode = 5000;
break;
default:
console.log("default");
logger.warn({
message: "invalid filter",
service: SERVICE_NAME,
method: "getChecks",
});
break;
}
}
@@ -170,7 +180,11 @@ const getTeamChecks = async (req) => {
checksQuery.statusCode = 5000;
break;
default:
console.log("default");
logger.warn({
message: "invalid filter",
service: SERVICE_NAME,
method: "getTeamChecks",
});
break;
}
}
@@ -248,9 +262,11 @@ const updateChecksTTL = async (teamId, ttl) => {
try {
await Check.collection.dropIndex("expiry_1");
} catch (error) {
logger.error("Failed to drop index", {
logger.error({
message: error.message,
service: SERVICE_NAME,
method: "updateChecksTTL",
stack: error.stack,
});
}
@@ -0,0 +1,16 @@
import HardwareCheck from "../../models/HardwareCheck.js";
const SERVICE_NAME = "hardwareCheckModule";
const createHardwareCheck = async (hardwareCheckData) => {
try {
const hardwareCheck = await new HardwareCheck({
...hardwareCheckData,
}).save();
return hardwareCheck;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createHardwareCheck";
throw error;
}
};
export { createHardwareCheck };
@@ -202,7 +202,6 @@ const deleteMaintenanceWindowByUserId = async (userId) => {
};
const editMaintenanceWindowById = async (maintenanceWindowId, maintenanceWindowData) => {
console.log(maintenanceWindowData);
try {
const editedMaintenanceWindow = MaintenanceWindow.findByIdAndUpdate(
maintenanceWindowId,
+1 -1
View File
@@ -68,7 +68,7 @@ const insertUser = async (userData, imageFile) => {
const getUserByEmail = async (email) => {
try {
// Need the password to be able to compare, removed .select()
// We can strip the hash before returing the user
// We can strip the hash before returning the user
const user = await UserModel.findOne({ email: email }).select("-profileImage");
if (user) {
return user;
+46 -59
View File
@@ -17,9 +17,11 @@ import maintenanceWindowRouter from "./routes/maintenanceWindowRoute.js";
import settingsRouter from "./routes/settingsRoute.js";
import { fileURLToPath } from "url";
import { connectDbAndRunServer } from "./configs/db.js";
import queueRouter from "./routes/queueRoute.js";
//JobQueue service and dependencies
import JobQueue from "./service/jobQueue.js";
import { Queue, Worker } from "bullmq";
//Network service and dependencies
import NetworkService from "./service/networkService.js";
@@ -34,9 +36,11 @@ import pkg from "handlebars";
const { compile } = pkg;
import mjml2html from "mjml";
// Settings Service and dependencies
import SettingsService from "./service/settingsService.js";
import AppSettings from "./db/models/AppSettings.js";
import db from "./db/mongo/MongoDB.js";
import { fetchMonitorCertificate } from "./controllers/controllerUtils.js";
const SERVICE_NAME = "Server";
let cleaningUp = false;
@@ -46,31 +50,11 @@ const __dirname = path.dirname(__filename);
const openApiSpec = JSON.parse(
fs.readFileSync(path.join(__dirname, "openapi.json"), "utf8")
);
const PORT = 5000;
// Need to wrap server setup in a function to handle async nature of JobQueue
const startApp = async () => {
// **************************
// Here is where we can swap out DBs easily. Spin up a mongoDB instance and try it out.
// Simply comment out the FakeDB and uncomment the MongoDB or vice versa.
// We can easily swap between any type of data source as long as the methods are implemented
//
// FakeDB
// const db = require("./db/FakeDb");
//
// MongoDB
// const db = require("./db/MongoDB");
//
// **************************
// const DB_TYPE = {
// MongoDB: async () => (await import("./db/mongo/MongoDB.js")).default,
// FakedDB: async () => (await import("./db/FakeDb.js")).default,
// };
// const db = DB_TYPE[process.env.DB_TYPE]
// ? DB_TYPE[process.env.DB_TYPE]()
// : require("./db/FakeDb");
// const db = DB_TYPE.MongoDB();
const app = express();
// middlewares
@@ -80,12 +64,8 @@ const startApp = async () => {
);
app.use(express.json());
app.use(helmet());
// **************************
// Make DB accessible anywhere we have a Request object
// By adding the DB to the request object, we can access it in any route
// Thus we do not need to import it in every route file, and we can easily swap out DBs as there is only one place to change it
// Same applies for JobQueue and emailService
// **************************
// Add db, jobQueue, emailService, and settingsService to request object for easy access
app.use((req, res, next) => {
req.db = db;
req.jobQueue = jobQueue;
@@ -109,27 +89,15 @@ const startApp = async () => {
//health check
app.use("/api/v1/healthy", (req, res) => {
try {
logger.info("Checking Health of the server.");
logger.info({ message: "Checking Health of the server." });
return res.status(200).json({ message: "Healthy" });
} catch (error) {
logger.error(error.message);
return res.status(500).json({ message: error.message });
}
});
app.use("/api/v1/mail", async (req, res) => {
try {
const id = await req.emailService.buildAndSendEmail(
"welcomeEmailTemplate",
{
name: "Alex",
},
"ajhollid@gmail.com",
"Welcome"
);
res.status(200).json({ success: true, msg: "Email sent", data: id });
} catch (error) {
logger.error(error.message);
logger.error({
message: error.message,
service: SERVICE_NAME,
method: "healthCheck",
stack: error.stack,
});
return res.status(500).json({ message: error.message });
}
});
@@ -141,8 +109,11 @@ const startApp = async () => {
app.use(handleErrors);
// Create services
await connectDbAndRunServer(app, db);
const settingsService = new SettingsService();
await db.connect();
app.listen(PORT, () => {
logger.info({ message: `server started on port:${PORT}` });
});
const settingsService = new SettingsService(AppSettings);
await settingsService.loadSettings();
const emailService = new EmailService(
@@ -155,22 +126,32 @@ const startApp = async () => {
logger
);
const networkService = new NetworkService(db, emailService, axios, ping, logger, http);
const jobQueue = await JobQueue.createJobQueue(db, networkService, settingsService);
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
Queue,
Worker
);
const cleanup = async () => {
if (cleaningUp) {
console.log("Already cleaning up");
logger.warn({ message: "Already cleaning up" });
return;
}
cleaningUp = true;
try {
console.log("Shutting down gracefully");
logger.info({ message: "shutting down gracefully" });
await jobQueue.obliterate();
console.log("Finished cleanup");
await db.disconnect();
logger.info({ message: "shut down gracefully" });
} catch (error) {
logger.error(errorMessages.JOB_QUEUE_DELETE_JOB, {
logger.error({
message: error.message,
service: SERVICE_NAME,
errorMsg: error.message,
method: "cleanup",
stack: error.stack,
});
}
process.exit(0);
@@ -181,5 +162,11 @@ const startApp = async () => {
};
startApp().catch((error) => {
console.log(error);
logger.error({
message: error.message,
service: SERVICE_NAME,
method: "startApp",
stack: error.stack,
});
process.exit(1);
});
+3 -1
View File
@@ -5,9 +5,11 @@ const handleErrors = (error, req, res, next) => {
const status = error.status || 500;
const message = error.message || errorMessages.FRIENDLY_ERROR;
const service = error.service || errorMessages.UNKNOWN_SERVICE;
logger.error(error.message, {
logger.error({
message: message,
service: service,
method: error.method,
stack: error.stack,
});
res.status(status).json({ success: false, msg: message });
};
+3 -1
View File
@@ -10,8 +10,10 @@ const verifyOwnership = (Model, paramName) => {
const doc = await Model.findById(documentId);
//If the document is not found, return a 404 error
if (!doc) {
logger.error(errorMessages.VERIFY_OWNER_NOT_FOUND, {
logger.error({
message: errorMessages.VERIFY_OWNER_NOT_FOUND,
service: SERVICE_NAME,
method: "verifyOwnership",
});
const error = new Error(errorMessages.VERIFY_OWNER_NOT_FOUND);
error.status = 404;
+9 -2
View File
@@ -37,8 +37,12 @@ const verifySuperAdmin = (req, res, next) => {
jwt.verify(parsedToken, jwtSecret, (err, decoded) => {
if (err) {
logger.error(errorMessages.INVALID_AUTH_TOKEN, {
logger.error({
message: err.message,
service: SERVICE_NAME,
method: "verifySuperAdmin",
stack: err.stack,
details: errorMessages.INVALID_AUTH_TOKEN,
});
return res
.status(401)
@@ -46,8 +50,11 @@ const verifySuperAdmin = (req, res, next) => {
}
if (decoded.role.includes("superadmin") === false) {
logger.error(errorMessages.INVALID_AUTH_TOKEN, {
logger.error({
message: errorMessages.INVALID_AUTH_TOKEN,
service: SERVICE_NAME,
method: "verifySuperAdmin",
stack: err.stack,
});
return res.status(401).json({ success: false, msg: errorMessages.UNAUTHORIZED });
}
+72 -1
View File
@@ -261,6 +261,77 @@
}
}
},
"/auth/refresh": {
"post": {
"tags": [
"auth"
],
"description": "Generates a new auth token if the refresh token is valid.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {}
}
}
},
"required": false
},
"parameters": [
{
"name": "x-refresh-token",
"in": "header",
"description": "Refresh token required to generate a new auth token.",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "authorization",
"in": "header",
"description": "Old access token, used to extract payload).",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "New access token generated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SuccessResponse"
}
}
}
},
"401": {
"description": "Unauthorized or invalid refresh token.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
}
},
"/auth/user/{userId}": {
"put": {
"tags": [
@@ -2502,4 +2573,4 @@
}
}
}
}
}
+509 -416
View File
File diff suppressed because it is too large. Load diff
+4 -4
View File
@@ -15,7 +15,7 @@
"@sendgrid/mail": "^8.1.3",
"axios": "^1.7.2",
"bcrypt": "^5.1.1",
"bullmq": "5.7.15",
"bullmq": "5.21.2",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
@@ -29,17 +29,17 @@
"multer": "1.4.5-lts.1",
"nodemailer": "^6.9.14",
"ping": "0.4.4",
"sharp": "0.33.4",
"sharp": "0.33.5",
"ssl-checker": "2.0.10",
"swagger-ui-express": "5.0.1",
"winston": "^3.13.0"
},
"devDependencies": {
"c8": "10.1.2",
"chai": "5.1.1",
"chai": "5.1.2",
"esm": "3.2.25",
"mocha": "10.7.3",
"nodemon": "3.1.0",
"nodemon": "3.1.7",
"prettier": "^3.3.3",
"sinon": "19.0.2"
}
+2
View File
@@ -11,6 +11,7 @@ const upload = multer();
import {
registerUser,
loginUser,
refreshAuthToken,
editUser,
requestRecovery,
validateRecovery,
@@ -23,6 +24,7 @@ import {
//Auth routes
router.post("/register", upload.single("profileImage"), registerUser);
router.post("/login", loginUser);
router.post("/refresh", refreshAuthToken);
router.put("/user/:userId", upload.single("profileImage"), verifyJWT, editUser);
router.get("/users/superadmin", checkSuperadminExists);
router.get("/users", verifyJWT, isAllowed(["admin", "superadmin"]), getAllUsers);
+13 -7
View File
@@ -44,9 +44,11 @@ class EmailService {
const templateContent = this.fs.readFileSync(templatePath, "utf8");
return this.compile(templateContent);
} catch (error) {
this.logger.error("Error loading Email templates", {
error,
service: this.SERVICE_NAME,
this.logger.error({
message: error.message,
service: SERVICE_NAME,
method: "loadTemplate",
stack: error.stack,
});
}
};
@@ -102,9 +104,11 @@ class EmailService {
const html = await this.mjml2html(mjml);
return html.html;
} catch (error) {
this.logger.error("Error building Email HTML", {
error,
this.logger.error({
message: error.message,
service: SERVICE_NAME,
method: "buildAndSendEmail",
stack: error.stack,
});
}
};
@@ -118,9 +122,11 @@ class EmailService {
});
return info;
} catch (error) {
this.logger.error("Error sending Email", {
error,
this.logger.error({
message: error.message,
service: SERVICE_NAME,
method: "sendEmail",
stack: error.stack,
});
}
};
+60 -45
View File
@@ -1,8 +1,5 @@
import { Queue, Worker, Job } from "bullmq";
const QUEUE_NAME = "monitors";
const JOBS_PER_WORKER = 5;
import logger from "../utils/logger.js";
import { errorMessages, successMessages } from "../utils/messages.js";
const SERVICE_NAME = "JobQueue";
/**
@@ -19,11 +16,13 @@ class JobQueue {
* @param {SettingsService} settingsService - The settings service
* @throws {Error}
*/
constructor(settingsService) {
const { redisHost, redisPort } = settingsService.getSettings();
constructor(settingsService, logger, Queue, Worker) {
const settings = settingsService.getSettings() || {};
const { redisHost = "127.0.0.1", redisPort = 6379 } = settings;
const connection = {
host: redisHost || "127.0.0.1",
port: redisPort || 6379,
host: redisHost,
port: redisPort,
};
this.connection = connection;
this.queue = new Queue(QUEUE_NAME, {
@@ -33,6 +32,8 @@ class JobQueue {
this.db = null;
this.networkService = null;
this.settingsService = settingsService;
this.logger = logger;
this.Worker = Worker;
}
/**
@@ -42,8 +43,15 @@ class JobQueue {
* @returns {Promise<JobQueue>} - Returns a new JobQueue
*
*/
static async createJobQueue(db, networkService, settingsService) {
const queue = new JobQueue(settingsService);
static async createJobQueue(
db,
networkService,
settingsService,
logger,
Queue,
Worker
) {
const queue = new JobQueue(settingsService, logger, Queue, Worker);
try {
queue.db = db;
queue.networkService = networkService;
@@ -69,7 +77,7 @@ class JobQueue {
* @returns {Worker} The newly created worker
*/
createWorker() {
const worker = new Worker(
const worker = new this.Worker(
QUEUE_NAME,
async (job) => {
try {
@@ -96,20 +104,22 @@ class JobQueue {
}
return acc;
}, false);
if (!maintenanceWindowActive) {
await this.networkService.getStatus(job);
} else {
logger.info(`Monitor ${monitorId} is in maintenance window`, {
this.logger.info({
message: `Monitor ${monitorId} is in maintenance window`,
service: SERVICE_NAME,
monitorId,
method: "createWorker",
});
}
} catch (error) {
logger.error(`Error processing job ${job.id}: ${error.message}`, {
this.logger.error({
message: error.message,
service: SERVICE_NAME,
jobId: job.id,
error: error,
method: "createWorker",
details: `Error processing job ${job.id}: ${error.message}`,
stack: error.stack,
});
}
},
@@ -169,11 +179,9 @@ class JobQueue {
}
return true;
}
if (workerStats.load > JOBS_PER_WORKER) {
// Find out how many more jobs we have than current workers can handle
const excessJobs = workerStats.jobs.length - this.workers.length * JOBS_PER_WORKER;
// Divide by jobs/worker to find out how many workers to add
const workersToAdd = Math.ceil(excessJobs / JOBS_PER_WORKER);
for (let i = 0; i < workersToAdd; i++) {
@@ -188,19 +196,19 @@ class JobQueue {
const workerCapacity = this.workers.length * JOBS_PER_WORKER;
const excessCapacity = workerCapacity - workerStats.jobs.length;
// Calculate how many workers to remove
const workersToRemove = Math.floor(excessCapacity / JOBS_PER_WORKER);
if (this.workers.length > 5) {
for (let i = 0; i < workersToRemove; i++) {
const worker = this.workers.pop();
try {
await worker.close();
} catch (error) {
// Catch the error instead of throwing it
logger.error(errorMessages.JOB_QUEUE_WORKER_CLOSE, {
service: SERVICE_NAME,
});
}
}
let workersToRemove = Math.floor(excessCapacity / JOBS_PER_WORKER); // Make sure there are always at least 5
while (workersToRemove > 0 && this.workers.length > 5) {
const worker = this.workers.pop();
workersToRemove--;
await worker.close().catch((error) => {
// Catch the error instead of throwing it
this.logger.error({
message: error.message,
service: SERVICE_NAME,
method: "scaleWorkers",
stack: error.stack,
});
});
}
return true;
}
@@ -252,7 +260,7 @@ class JobQueue {
*/
async addJob(jobName, payload) {
try {
console.log("Adding job", payload?.url ?? "No URL");
this.logger.info({ message: `Adding job ${payload?.url ?? "No URL"}` });
// Execute job immediately
await this.queue.add(jobName, payload);
await this.queue.add(jobName, payload, {
@@ -278,20 +286,24 @@ class JobQueue {
*/
async deleteJob(monitor) {
try {
const deleted = await this.queue.removeRepeatable(monitor._id, {
const wasDeleted = await this.queue.removeRepeatable(monitor._id, {
every: monitor.interval,
});
if (deleted) {
logger.info(successMessages.JOB_QUEUE_DELETE_JOB, {
if (wasDeleted === true) {
this.logger.info({
message: successMessages.JOB_QUEUE_DELETE_JOB,
service: SERVICE_NAME,
jobId: monitor.id,
method: "deleteJob",
details: `Deleted job ${monitor._id}`,
});
const workerStats = await this.getWorkerStats();
await this.scaleWorkers(workerStats);
} else {
logger.error(errorMessages.JOB_QUEUE_DELETE_JOB, {
this.logger.error({
message: errorMessages.JOB_QUEUE_DELETE_JOB,
service: SERVICE_NAME,
jobId: monitor.id,
method: "deleteJob",
details: `Failed to delete job ${monitor._id}`,
});
}
} catch (error) {
@@ -311,11 +323,16 @@ class JobQueue {
delayed: await this.queue.getDelayedCount(),
repeatableJobs: (await this.queue.getRepeatableJobs()).length,
};
this.logger.info({
message: metrics,
});
return metrics;
} catch (error) {
logger.error("Failed to retrieve job queue metrics", {
this.logger.error({
message: error.message,
service: SERVICE_NAME,
errorMsg: error.message,
method: "getMetrics",
stack: error.stack,
});
}
}
@@ -327,7 +344,7 @@ class JobQueue {
async obliterate() {
try {
let metrics = await this.getMetrics();
console.log(metrics);
this.logger.info({ message: metrics });
await this.queue.pause();
const jobs = await this.getJobs();
@@ -343,10 +360,8 @@ class JobQueue {
await this.queue.obliterate();
metrics = await this.getMetrics();
console.log(metrics);
logger.info(successMessages.JOB_QUEUE_OBLITERATE, {
service: SERVICE_NAME,
});
this.logger.info({ message: metrics });
this.logger.info({ message: successMessages.JOB_QUEUE_OBLITERATE });
return true;
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
+106 -19
View File
@@ -24,6 +24,7 @@ class NetworkService {
this.TYPE_PING = "ping";
this.TYPE_HTTP = "http";
this.TYPE_PAGESPEED = "pagespeed";
this.TYPE_HARDWARE = "hardware";
this.SERVICE_NAME = "NetworkService";
this.NETWORK_ERROR = 5000;
this.axios = axios;
@@ -55,10 +56,12 @@ class NetworkService {
}
}
} catch (error) {
this.logger.error(error.message, {
method: "handleNotification",
this.logger.error({
message: error.message,
service: this.SERVICE_NAME,
monitorId: monitor._id,
method: "handleNotification",
details: `notification error for monitor: ${monitor._id}`,
stack: error.stack,
});
}
}
@@ -78,19 +81,18 @@ class NetworkService {
try {
monitor = await this.db.getMonitorById(_id);
} catch (error) {
this.logger.error({
message: error.message,
service: this.SERVICE_NAME,
method: "handleStatusUpdate",
stack: error.stack,
details: `monitor lookup error for monitor: ${_id}`,
});
return;
}
// Otherwise, try to update monitor status
try {
if (monitor === null || monitor === undefined) {
this.logger.error(`Null Monitor: ${_id}`, {
method: "handleStatusUpdate",
service: this.SERVICE_NAME,
jobId: job.id,
});
return;
}
if (monitor.status === undefined || monitor.status !== isAlive) {
const oldStatus = monitor.status;
monitor.status = isAlive;
@@ -101,10 +103,12 @@ class NetworkService {
}
}
} catch (error) {
this.logger.error(error.message, {
method: "handleStatusUpdate",
this.logger.error({
message: error.message,
service: this.SERVICE_NAME,
jobId: job.id,
method: "handleStatusUpdate",
stack: error.stack,
details: `status update error for monitor: ${_id}`,
});
}
}
@@ -293,6 +297,85 @@ class NetworkService {
}
}
async handleHardware(job) {
const url = job.data.url;
let isAlive;
//TODO Fetch hardware data
//For now, fake hardware data:
const hardwareData = {
monitorId: job.data._id,
cpu: {
physical_core: 1,
logical_core: 1,
frequency: 266,
temperature: null,
free_percent: null,
usage_percent: null,
},
memory: {
total_bytes: 4,
available_bytes: 4,
used_bytes: 2,
usage_percent: 0.5,
},
disk: [
{
read_speed_bytes: 3,
write_speed_bytes: 3,
total_bytes: 10,
free_bytes: 2,
usage_percent: 0.8,
},
],
host: {
os: "Linux",
platform: "Ubuntu",
kernel_version: "24.04",
},
};
try {
isAlive = true;
this.logAndStoreCheck(hardwareData, this.db.createHardwareCheck);
} catch (error) {
isAlive = false;
const nullData = {
monitorId: job.data._id,
cpu: {
physical_core: 0,
logical_core: 0,
frequency: 0,
temperature: 0,
free_percent: 0,
usage_percent: 0,
},
memory: {
total_bytes: 0,
available_bytes: 0,
used_bytes: 0,
usage_percent: 0,
},
disk: [
{
read_speed_bytes: 0,
write_speed_bytes: 0,
total_bytes: 0,
free_bytes: 0,
usage_percent: 0,
},
],
host: {
os: "",
platform: "",
kernel_version: "",
},
};
this.logAndStoreCheck(nullData, this.db.createHardwareCheck);
} finally {
this.handleStatusUpdate(job, isAlive);
}
}
/**
* Retrieves the status of a given job based on its type.
* For unsupported job types, it logs an error and returns false.
@@ -308,12 +391,15 @@ class NetworkService {
return await this.handleHttp(job);
case this.TYPE_PAGESPEED:
return await this.handlePagespeed(job);
case this.TYPE_HARDWARE:
return await this.handleHardware(job);
default:
this.logger.error(`Unsupported type: ${job.data.type}`, {
this.logger.error({
message: `Unsupported type: ${job.data.type}`,
service: this.SERVICE_NAME,
method: "getStatus",
jobId: job.id,
});
return false;
}
}
@@ -335,11 +421,12 @@ class NetworkService {
}
throw new Error();
} catch (error) {
this.logger.error(`Error writing check for ${data.monitorId}`, {
this.logger.error({
message: error.message,
service: this.SERVICE_NAME,
method: "logAndStoreCheck",
monitorId: data.monitorId,
error: error,
details: `Error writing check for ${data.monitorId}`,
stack: error.stack,
});
}
}
+3 -7
View File
@@ -1,4 +1,3 @@
import AppSettings from "../db/models/AppSettings.js";
const SERVICE_NAME = "SettingsService";
const envConfig = {
logLevel: undefined,
@@ -30,7 +29,8 @@ class SettingsService {
* Constructs a new SettingsService
* @constructor
* @throws {Error}
*/ constructor() {
*/ constructor(appSettings) {
this.appSettings = appSettings;
this.settings = { ...envConfig };
}
/**
@@ -40,7 +40,7 @@ class SettingsService {
* @throws Will throw an error if settings are not found in the database or if settings have not been loaded.
*/ async loadSettings() {
try {
const dbSettings = await AppSettings.findOne();
const dbSettings = await this.appSettings.findOne();
if (!this.settings) {
throw new Error("Settings not found");
}
@@ -51,10 +51,6 @@ class SettingsService {
this.settings[key] = dbSettings[key];
}
}
if (!this.settings) {
throw new Error("Settings not found");
}
return this.settings;
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
@@ -2,6 +2,7 @@ import {
issueToken,
registerUser,
loginUser,
refreshAuthToken,
editUser,
checkSuperadminExists,
requestRecovery,
@@ -13,8 +14,9 @@ import {
import jwt from "jsonwebtoken";
import { errorMessages, successMessages } from "../../utils/messages.js";
import sinon from "sinon";
import { tokenType } from "../../utils/utils.js";
import { getTokenFromHeaders, tokenType } from "../../utils/utils.js";
import logger from "../../utils/logger.js";
import e from "cors";
describe("Auth Controller - issueToken", () => {
let stub;
@@ -180,7 +182,7 @@ describe("Auth Controller - registerUser", () => {
req.emailService.buildAndSendEmail.rejects(new Error("emailService error"));
await registerUser(req, res, next);
expect(logger.error.calledOnce).to.be.true;
expect(logger.error.firstCall.args[1].error).to.equal("emailService error");
expect(logger.error.firstCall.args[0].message).to.equal("emailService error");
});
it("should return a success message and data if all operations are successful", async () => {
const user = { _id: "123" };
@@ -303,6 +305,97 @@ describe("Auth Controller - loginUser", () => {
});
});
describe("Auth Controller - refreshAuthToken", () => {
let req, res, next, issueTokenStub;
beforeEach(() => {
req = {
headers: {
"x-refresh-token": "valid_refresh_token",
authorization: "Bearer old_auth_token",
},
settingsService: {
getSettings: sinon.stub().resolves({
jwtSecret: "my_secret",
refreshTokenSecret: "my_refresh_secret",
}),
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
sinon.stub(jwt, "verify");
issueTokenStub = sinon.stub().returns("new_auth_token");
sinon.replace({ issueToken }, "issueToken", issueTokenStub);
});
afterEach(() => {
sinon.restore();
});
it("should reject if no refresh token is provided", async () => {
delete req.headers["x-refresh-token"];
await refreshAuthToken(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal(errorMessages.NO_REFRESH_TOKEN);
expect(next.firstCall.args[0].status).to.equal(401);
});
it("should reject if the refresh token is invalid", async () => {
jwt.verify.yields(new Error("invalid token"));
await refreshAuthToken(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal(errorMessages.INVALID_REFRESH_TOKEN);
expect(next.firstCall.args[0].status).to.equal(401);
});
it("should reject if the refresh token is expired", async () => {
const error = new Error("Token expired");
error.name = "TokenExpiredError";
jwt.verify.yields(error);
await refreshAuthToken(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal(errorMessages.EXPIRED_REFRESH_TOKEN);
expect(next.firstCall.args[0].status).to.equal(401);
});
it("should reject if settingsService.getSettings fails", async () => {
req.settingsService.getSettings.rejects(
new Error("settingsService.getSettings error")
);
await refreshAuthToken(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal("settingsService.getSettings error");
});
it("should generate a new auth token if the refresh token is valid", async () => {
const decodedPayload = { expiresIn: "60" };
jwt.verify.callsFake(() => {
return decodedPayload;
});
await refreshAuthToken(req, res, next);
expect(res.status.calledWith(200)).to.be.true;
expect(
res.json.calledWith({
success: true,
msg: successMessages.AUTH_TOKEN_REFRESHED,
data: {
user: decodedPayload,
token: sinon.match.string,
refreshToken: "valid_refresh_token",
},
})
).to.be.true;
});
});
describe("Auth Controller - editUser", async () => {
let req, res, next, stub, user;
beforeEach(() => {
@@ -374,7 +467,7 @@ describe("Auth Controller - editUser", async () => {
});
await editUser(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(403);
expect(next.firstCall.args[0].status).to.equal(401);
expect(next.firstCall.args[0].message).to.equal(
errorMessages.AUTH_INCORRECT_PASSWORD
);
@@ -719,9 +812,9 @@ describe("Auth Controller - deleteUser", () => {
afterEach(() => {
sinon.restore();
});
it("should return 404 if user is not found", async () => {
it("should throw an error if user is not found", async () => {
jwt.decode.returns({ email: "test@example.com" });
req.db.getUserByEmail.resolves(null);
req.db.getUserByEmail.throws(new Error(errorMessages.DB_USER_NOT_FOUND));
await deleteUser(req, res, next);
@@ -462,38 +462,40 @@ describe("Monitor Controller - createMonitor", () => {
describe("Monitor Controllor - checkEndpointResolution", () => {
let req, res, next, dnsResolveStub;
beforeEach(() => {
req = { query: { monitorURL: 'https://example.com' } };
req = { query: { monitorURL: "https://example.com" } };
res = { status: sinon.stub().returnsThis(), json: sinon.stub() };
next = sinon.stub();
dnsResolveStub = sinon.stub(dns, 'resolve');
dnsResolveStub = sinon.stub(dns, "resolve");
});
afterEach(() => {
dnsResolveStub.restore();
});
it('should resolve the URL successfully', async () => {
it("should resolve the URL successfully", async () => {
dnsResolveStub.callsFake((hostname, callback) => callback(null));
await checkEndpointResolution(req, res, next);
expect(res.status.calledWith(200)).to.be.true;
expect(res.json.calledWith({
success: true,
msg: 'URL resolved successfully',
})).to.be.true;
expect(
res.json.calledWith({
success: true,
msg: "URL resolved successfully",
})
).to.be.true;
expect(next.called).to.be.false;
});
it("should return an error if DNS resolution fails", async () => {
const dnsError = new Error("DNS resolution failed");
dnsError.code = 'ENOTFOUND';
dnsError.code = "ENOTFOUND";
dnsResolveStub.callsFake((hostname, callback) => callback(dnsError));
await checkEndpointResolution(req, res, next);
expect(next.calledOnce).to.be.true;
const errorPassedToNext = next.getCall(0).args[0];
expect(errorPassedToNext).to.be.an.instanceOf(Error);
expect(errorPassedToNext.message).to.include('DNS resolution failed');
expect(errorPassedToNext.code).to.equal('ENOTFOUND');
const errorPassedToNext = next.getCall(0).args[0];
expect(errorPassedToNext).to.be.an.instanceOf(Error);
expect(errorPassedToNext.message).to.include("DNS resolution failed");
expect(errorPassedToNext.code).to.equal("ENOTFOUND");
expect(errorPassedToNext.status).to.equal(500);
});
it('should reject with an error if query validation fails', async () => {
req.query.monitorURL = 'invalid-url';
it("should reject with an error if query validation fails", async () => {
req.query.monitorURL = "invalid-url";
await checkEndpointResolution(req, res, next);
expect(next.calledOnce).to.be.true;
const error = next.getCall(0).args[0];
@@ -551,14 +553,9 @@ describe("Monitor Controller - deleteMonitor", () => {
req.jobQueue.deleteJob.rejects(error);
await deleteMonitor(req, res, next);
expect(logger.error.calledOnce).to.be.true;
expect(logger.error.firstCall.args[0]).to.equal(
expect(logger.error.firstCall.args[0].message).to.equal(
`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`
);
expect(logger.error.firstCall.args[1]).to.deep.equal({
method: "deleteMonitor",
service: SERVICE_NAME,
error: error.message,
});
});
it("should log an error if deleteChecks throws an error", async () => {
const error = new Error("Checks error");
@@ -567,14 +564,9 @@ describe("Monitor Controller - deleteMonitor", () => {
req.db.deleteChecks.rejects(error);
await deleteMonitor(req, res, next);
expect(logger.error.calledOnce).to.be.true;
expect(logger.error.firstCall.args[0]).to.equal(
expect(logger.error.firstCall.args[0].message).to.equal(
`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`
);
expect(logger.error.firstCall.args[1]).to.deep.equal({
method: "deleteMonitor",
service: SERVICE_NAME,
error: error.message,
});
});
it("should log an error if deletePageSpeedChecksByMonitorId throws an error", async () => {
const error = new Error("PageSpeed error");
@@ -583,14 +575,9 @@ describe("Monitor Controller - deleteMonitor", () => {
req.db.deletePageSpeedChecksByMonitorId.rejects(error);
await deleteMonitor(req, res, next);
expect(logger.error.calledOnce).to.be.true;
expect(logger.error.firstCall.args[0]).to.equal(
expect(logger.error.firstCall.args[0].message).to.equal(
`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`
);
expect(logger.error.firstCall.args[1]).to.deep.equal({
method: "deleteMonitor",
service: SERVICE_NAME,
error: error.message,
});
});
it("should log an error if deleteNotificationsByMonitorId throws an error", async () => {
const error = new Error("Notifications error");
@@ -599,14 +586,9 @@ describe("Monitor Controller - deleteMonitor", () => {
req.db.deleteNotificationsByMonitorId.rejects(error);
await deleteMonitor(req, res, next);
expect(logger.error.calledOnce).to.be.true;
expect(logger.error.firstCall.args[0]).to.equal(
expect(logger.error.firstCall.args[0].message).to.equal(
`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`
);
expect(logger.error.firstCall.args[1]).to.deep.equal({
method: "deleteMonitor",
service: SERVICE_NAME,
error: error.message,
});
});
it("should return success message if all operations succeed", async () => {
const monitor = { name: "test_monitor", _id: "123" };
@@ -681,18 +663,7 @@ describe("Monitor Controller - deleteAllMonitors", () => {
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal("DB error");
});
it("should log an error if deleteJob throws an error", async () => {
const monitors = [{ name: "test_monitor", _id: "123" }];
req.settingsService.getSettings.returns({ jwtSecret: "my_secret" });
req.db.deleteAllMonitors.returns({ monitors, deletedCount: 1 });
const error = new Error("Job error");
req.jobQueue.deleteJob.rejects(error);
await deleteAllMonitors(req, res, next);
expect(logger.error.calledOnce).to.be.true;
expect(logger.error.firstCall.args[0]).to.deep.equal(
`Error deleting associated records for monitor ${monitors[0]._id} with name ${monitors[0].name}`
);
});
it("should log an error if deleteChecks throws an error", async () => {
const monitors = [{ name: "test_monitor", _id: "123" }];
req.settingsService.getSettings.returns({ jwtSecret: "my_secret" });
@@ -701,7 +672,7 @@ describe("Monitor Controller - deleteAllMonitors", () => {
req.db.deleteChecks.rejects(error);
await deleteAllMonitors(req, res, next);
expect(logger.error.calledOnce).to.be.true;
expect(logger.error.firstCall.args[0]).to.deep.equal(
expect(logger.error.firstCall.args[0].message).to.equal(
`Error deleting associated records for monitor ${monitors[0]._id} with name ${monitors[0].name}`
);
});
@@ -713,7 +684,7 @@ describe("Monitor Controller - deleteAllMonitors", () => {
req.db.deletePageSpeedChecksByMonitorId.rejects(error);
await deleteAllMonitors(req, res, next);
expect(logger.error.calledOnce).to.be.true;
expect(logger.error.firstCall.args[0]).to.deep.equal(
expect(logger.error.firstCall.args[0].message).to.equal(
`Error deleting associated records for monitor ${monitors[0]._id} with name ${monitors[0].name}`
);
});
@@ -725,7 +696,7 @@ describe("Monitor Controller - deleteAllMonitors", () => {
req.db.deleteNotificationsByMonitorId.rejects(error);
await deleteAllMonitors(req, res, next);
expect(logger.error.calledOnce).to.be.true;
expect(logger.error.firstCall.args[0]).to.deep.equal(
expect(logger.error.firstCall.args[0].message).to.equal(
`Error deleting associated records for monitor ${monitors[0]._id} with name ${monitors[0].name}`
);
});
+5 -12
View File
@@ -93,14 +93,7 @@ describe("EmailService - Constructor", () => {
loggerMock
);
expect(loggerMock.error.called).to.be.true;
const errorCalls = loggerMock.error.getCalls();
const errorCall = errorCalls.find(
(call) => call.args[0] === "Error loading Email templates"
);
expect(errorCall).to.not.be.undefined;
expect(emailService.settingsService).to.equal(settingsServiceMock);
expect(emailService.templateLookup.welcomeEmailTemplate).to.be.undefined;
expect(emailService.templateLookup.employeeActivationTemplate).to.be.undefined;
expect(loggerMock.error.firstCall.args[0].message).to.equal("File read error");
});
});
@@ -182,7 +175,7 @@ describe("EmailService - buildAndSendEmail", () => {
"Welcome"
);
expect(loggerMock.error.calledOnce).to.be.true;
expect(loggerMock.error.getCall(0).args[0]).to.equal("Error building Email HTML");
expect(loggerMock.error.getCall(0).args[0].message).to.equal("MJML error");
});
it("should log error if sending email fails", async () => {
@@ -194,7 +187,7 @@ describe("EmailService - buildAndSendEmail", () => {
"Welcome"
);
expect(loggerMock.error.calledOnce).to.be.true;
expect(loggerMock.error.getCall(0).args[0]).to.equal("Error sending Email");
expect(loggerMock.error.getCall(0).args[0].message).to.equal("SMTP error");
});
it("should log error if both building HTML and sending email fail", async () => {
@@ -210,8 +203,8 @@ describe("EmailService - buildAndSendEmail", () => {
expect(messageId).to.be.undefined;
expect(loggerMock.error.calledTwice).to.be.true;
expect(loggerMock.error.getCall(0).args[0]).to.equal("Error building Email HTML");
expect(loggerMock.error.getCall(1).args[0]).to.equal("Error sending Email");
expect(loggerMock.error.getCall(0).args[0].message).to.equal("MJML error");
expect(loggerMock.error.getCall(1).args[0].message).to.equal("SMTP error");
});
it("should log an error if buildHtml fails", async () => {});
+741
View File
@@ -0,0 +1,741 @@
import sinon from "sinon";
import JobQueue from "../../service/jobQueue.js";
import { log } from "console";
class QueueStub {
constructor(queueName, options) {
this.queueName = queueName;
this.options = options;
this.workers = [];
this.jobs = [];
}
// Add any methods that are expected to be called on the Queue instance
add(job) {
this.jobs.push(job);
}
removeRepeatable(id) {
const removedJob = this.jobs.find((job) => job.data._id === id);
this.jobs = this.jobs.filter((job) => job.data._id !== id);
if (removedJob) {
return true;
}
return false;
}
getRepeatableJobs() {
return this.jobs;
}
async getJobs() {
return this.jobs;
}
}
class WorkerStub {
constructor(QUEUE_NAME, workerTask) {
this.queueName = QUEUE_NAME;
this.workerTask = async () => workerTask({ data: { _id: 1 } });
}
async close() {
return true;
}
}
describe("JobQueue", () => {
let settingsService, logger, db, networkService;
beforeEach(() => {
settingsService = { getSettings: sinon.stub() };
logger = { error: sinon.stub(), info: sinon.stub() };
db = {
getAllMonitors: sinon.stub().returns([]),
getMaintenanceWindowsByMonitorId: sinon.stub().returns([]),
};
networkService = { getStatus: sinon.stub() };
});
afterEach(() => {
sinon.restore();
});
describe("createJobQueue", () => {
it("should create a new JobQueue and add jobs for active monitors", async () => {
db.getAllMonitors.returns([
{ id: 1, isActive: true },
{ id: 2, isActive: true },
]);
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
// There should be double the jobs, as one is meant to be instantly executed
// And one is meant to be enqueued
expect(jobQueue.queue.jobs.length).to.equal(4);
});
it("should reject with an error if an error occurs", async () => {
db.getAllMonitors.throws("Error");
try {
await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
} catch (error) {
expect(error.service).to.equal("JobQueue");
expect(error.method).to.equal("createJobQueue");
}
});
it("should reject with an error if an error occurs, should not overwrite error data", async () => {
const error = new Error("Error");
error.service = "otherService";
error.method = "otherMethod";
db.getAllMonitors.throws(error);
try {
await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
} catch (error) {
expect(error.service).to.equal("otherService");
expect(error.method).to.equal("otherMethod");
}
});
});
describe("Constructor", () => {
it("should construct a new JobQueue with default port and host if not provided", () => {
settingsService.getSettings.returns({});
const jobQueue = new JobQueue(settingsService, logger, QueueStub, WorkerStub);
expect(jobQueue.connection.host).to.equal("127.0.0.1");
expect(jobQueue.connection.port).to.equal(6379);
});
it("should construct a new JobQueue with provided port and host", () => {
settingsService.getSettings.returns({ redisHost: "localhost", redisPort: 1234 });
const jobQueue = new JobQueue(settingsService, logger, QueueStub, WorkerStub);
expect(jobQueue.connection.host).to.equal("localhost");
expect(jobQueue.connection.port).to.equal(1234);
});
});
describe("createWorker", () => {
it("should create a new worker", async () => {
const jobQueue = new JobQueue(settingsService, logger, QueueStub, WorkerStub);
const worker = jobQueue.createWorker();
expect(worker).to.be.instanceOf(WorkerStub);
});
it("worker should handle a maintenanceWindow error", async () => {
db.getMaintenanceWindowsByMonitorId.throws("Error");
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
const worker = jobQueue.createWorker();
await worker.workerTask();
expect(logger.error.calledOnce).to.be.true;
});
it("worker should handle a maintenanceWindow that is not active", async () => {
db.getMaintenanceWindowsByMonitorId.returns([
{ start: 123, end: 123, repeat: 123456 },
]);
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
const worker = jobQueue.createWorker();
await worker.workerTask();
expect(networkService.getStatus.calledOnce).to.be.true;
});
it("worker should handle a maintenanceWindow that is active", async () => {
db.getMaintenanceWindowsByMonitorId.returns([
{
active: true,
start: new Date(Date.now() - 1000).toISOString(),
end: new Date(Date.now() + 1000).toISOString(),
repeat: 0,
},
]);
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
const worker = jobQueue.createWorker();
await worker.workerTask();
expect(networkService.getStatus.calledOnce).to.be.false;
});
it("worker should handle a maintenanceWindow that is active, has a repeat, but is not in maintenance zone", async () => {
db.getMaintenanceWindowsByMonitorId.returns([
{
active: true,
start: new Date(Date.now() - 10000).toISOString(),
end: new Date(Date.now() + 5000).toISOString(),
repeat: 10000,
},
]);
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
const worker = jobQueue.createWorker();
await worker.workerTask();
expect(networkService.getStatus.calledOnce).to.be.true;
});
});
describe("getWorkerStats", () => {
it("should throw an error if getRepeatable Jobs fails", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.queue.getRepeatableJobs = async () => {
throw new Error("Error");
};
try {
const stats = await jobQueue.getWorkerStats();
} catch (error) {
expect(error.service).to.equal("JobQueue");
expect(error.method).to.equal("getWorkerStats");
}
});
it("should throw an error if getRepeatable Jobs fails but respect existing error data", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.queue.getRepeatableJobs = async () => {
const error = new Error("Existing Error");
error.service = "otherService";
error.method = "otherMethod";
throw error;
};
try {
await jobQueue.getWorkerStats();
} catch (error) {
expect(error.service).to.equal("otherService");
expect(error.method).to.equal("otherMethod");
}
});
});
describe("scaleWorkers", () => {
it("should scale workers to 5 if no workers", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
expect(jobQueue.workers.length).to.equal(5);
});
it("should scale workers up", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.scaleWorkers({
load: 100,
jobs: Array.from({ length: 100 }, (_, i) => i + 1),
});
expect(jobQueue.workers.length).to.equal(20);
});
it("should scale workers down, even with error of worker.close fails", async () => {
WorkerStub.prototype.close = async () => {
throw new Error("Error");
};
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
await jobQueue.scaleWorkers({
load: 100,
jobs: Array.from({ length: 100 }, (_, i) => i + 1),
});
const res = await jobQueue.scaleWorkers({
load: 0,
jobs: [],
});
expect(jobQueue.workers.length).to.equal(5);
});
it("should scale workers down", async () => {
WorkerStub.prototype.close = async () => {
return true;
};
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
await jobQueue.scaleWorkers({
load: 40,
jobs: Array.from({ length: 40 }, (_, i) => i + 1),
});
const res = await jobQueue.scaleWorkers({
load: 0,
jobs: [],
});
expect(jobQueue.workers.length).to.equal(5);
});
it("should return false if scaling doesn't happen", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
const res = await jobQueue.scaleWorkers({ load: 5 });
expect(jobQueue.workers.length).to.equal(5);
expect(res).to.be.false;
});
});
describe("getJobs", async () => {
it("should return jobs", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
const jobs = await jobQueue.getJobs();
expect(jobs.length).to.equal(0);
});
it("should throw an error if getRepeatableJobs fails", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
try {
jobQueue.queue.getRepeatableJobs = async () => {
throw new Error("error");
};
await jobQueue.getJobs(true);
} catch (error) {
expect(error.service).to.equal("JobQueue");
expect(error.method).to.equal("getJobs");
}
});
it("should throw an error if getRepeatableJobs fails but respect existing error data", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
try {
jobQueue.queue.getRepeatableJobs = async () => {
const error = new Error("Existing error");
error.service = "otherService";
error.method = "otherMethod";
throw error;
};
await jobQueue.getJobs(true);
} catch (error) {
expect(error.service).to.equal("otherService");
expect(error.method).to.equal("otherMethod");
}
});
});
describe("getJobStats", async () => {
it("should return job stats for no jobs", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
const jobStats = await jobQueue.getJobStats();
expect(jobStats).to.deep.equal({ jobs: [], workers: 5 });
});
it("should return job stats for jobs", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.queue.getJobs = async () => {
return [{ data: { url: "test" }, getState: async () => "completed" }];
};
const jobStats = await jobQueue.getJobStats();
expect(jobStats).to.deep.equal({
jobs: [{ url: "test", state: "completed" }],
workers: 5,
});
});
it("should reject with an error if mapping jobs fails", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.queue.getJobs = async () => {
return [
{
data: { url: "test" },
getState: async () => {
throw new Error("Mapping Error");
},
},
];
};
try {
await jobQueue.getJobStats();
} catch (error) {
expect(error.message).to.equal("Mapping Error");
expect(error.service).to.equal("JobQueue");
expect(error.method).to.equal("getJobStats");
}
});
it("should reject with an error if mapping jobs fails but respect existing error data", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.queue.getJobs = async () => {
return [
{
data: { url: "test" },
getState: async () => {
const error = new Error("Mapping Error");
error.service = "otherService";
error.method = "otherMethod";
throw error;
},
},
];
};
try {
await jobQueue.getJobStats();
} catch (error) {
expect(error.message).to.equal("Mapping Error");
expect(error.service).to.equal("otherService");
expect(error.method).to.equal("otherMethod");
}
});
});
describe("addJob", async () => {
it("should add a job to the queue", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.addJob("test", { url: "test" });
expect(jobQueue.queue.jobs.length).to.equal(1);
});
it("should reject with an error if adding fails", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.queue.add = async () => {
throw new Error("Error adding job");
};
try {
await jobQueue.addJob("test", { url: "test" });
} catch (error) {
expect(error.message).to.equal("Error adding job");
expect(error.service).to.equal("JobQueue");
expect(error.method).to.equal("addJob");
}
});
it("should reject with an error if adding fails but respect existing error data", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.queue.add = async () => {
const error = new Error("Error adding job");
error.service = "otherService";
error.method = "otherMethod";
throw error;
};
try {
await jobQueue.addJob("test", { url: "test" });
} catch (error) {
expect(error.message).to.equal("Error adding job");
expect(error.service).to.equal("otherService");
expect(error.method).to.equal("otherMethod");
}
});
});
describe("deleteJob", async () => {
it("should delete a job from the queue", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.getWorkerStats = sinon.stub().returns({ load: 1, jobs: [{}] });
jobQueue.scaleWorkers = sinon.stub();
const monitor = { _id: 1 };
const job = { data: monitor };
jobQueue.queue.jobs = [job];
await jobQueue.deleteJob(monitor);
// expect(jobQueue.queue.jobs.length).to.equal(0);
// expect(logger.info.calledOnce).to.be.true;
// expect(jobQueue.getWorkerStats.calledOnce).to.be.true;
// expect(jobQueue.scaleWorkers.calledOnce).to.be.true;
});
it("should log an error if job is not found", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.getWorkerStats = sinon.stub().returns({ load: 1, jobs: [{}] });
jobQueue.scaleWorkers = sinon.stub();
const monitor = { _id: 1 };
const job = { data: monitor };
jobQueue.queue.jobs = [job];
await jobQueue.deleteJob({ id_: 2 });
expect(logger.error.calledOnce).to.be.true;
});
it("should reject with an error if removeRepeatable fails", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.queue.removeRepeatable = async () => {
const error = new Error("removeRepeatable error");
throw error;
};
try {
await jobQueue.deleteJob({ _id: 1 });
} catch (error) {
expect(error.message).to.equal("removeRepeatable error");
expect(error.service).to.equal("JobQueue");
expect(error.method).to.equal("deleteJob");
}
});
it("should reject with an error if removeRepeatable fails but respect existing error data", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.queue.removeRepeatable = async () => {
const error = new Error("removeRepeatable error");
error.service = "otherService";
error.method = "otherMethod";
throw error;
};
try {
await jobQueue.deleteJob({ _id: 1 });
} catch (error) {
expect(error.message).to.equal("removeRepeatable error");
expect(error.service).to.equal("otherService");
expect(error.method).to.equal("otherMethod");
}
});
});
describe("getMetrics", () => {
it("should return metrics for the job queue", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.queue.getWaitingCount = async () => 1;
jobQueue.queue.getActiveCount = async () => 2;
jobQueue.queue.getCompletedCount = async () => 3;
jobQueue.queue.getFailedCount = async () => 4;
jobQueue.queue.getDelayedCount = async () => 5;
jobQueue.queue.getRepeatableJobs = async () => [1, 2, 3];
const metrics = await jobQueue.getMetrics();
expect(metrics).to.deep.equal({
waiting: 1,
active: 2,
completed: 3,
failed: 4,
delayed: 5,
repeatableJobs: 3,
});
});
it("should log an error if metrics operations fail", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.queue.getWaitingCount = async () => {
throw new Error("Error");
};
await jobQueue.getMetrics();
expect(logger.error.calledOnce).to.be.true;
expect(logger.error.firstCall.args[0].message).to.equal("Error");
});
});
describe("obliterate", () => {
it("should return true if obliteration is successful", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.queue.pause = async () => true;
jobQueue.getJobs = async () => [{ key: 1, id: 1 }];
jobQueue.queue.removeRepeatableByKey = async () => true;
jobQueue.queue.remove = async () => true;
jobQueue.queue.obliterate = async () => true;
const obliteration = await jobQueue.obliterate();
expect(obliteration).to.be.true;
});
it("should throw an error if obliteration fails", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.getMetrics = async () => {
throw new Error("Error");
};
try {
await jobQueue.obliterate();
} catch (error) {
expect(error.service).to.equal("JobQueue");
expect(error.method).to.equal("obliterate");
}
});
it("should throw an error if obliteration fails but respect existing error data", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.getMetrics = async () => {
const error = new Error("Error");
error.service = "otherService";
error.method = "otherMethod";
throw error;
};
try {
await jobQueue.obliterate();
} catch (error) {
expect(error.service).to.equal("otherService");
expect(error.method).to.equal("otherMethod");
}
});
});
});
+146 -41
View File
@@ -105,13 +105,7 @@ describe("NetworkService - handleNotification", () => {
await networkService.handleNotification(monitor, true);
expect(loggerMock.error.calledOnce).to.be.true;
expect(
loggerMock.error.calledWith("Database error", {
method: "handleNotification",
service: networkService.SERVICE_NAME,
monitorId: monitor._id,
})
).to.be.true;
expect(loggerMock.error.firstCall.args[0].message).to.equal("Database error");
});
});
@@ -160,13 +154,13 @@ describe("NetworkService - handleStatusUpdate", function () {
expect(res).to.be.undefined;
});
it("should log an error if monitor is null", async function () {
dbMock.getMonitorById.resolves(null);
it("should log an error if finding monitor fails", async function () {
dbMock.getMonitorById.throws(new Error("Database error"));
await networkService.handleStatusUpdate({ data: { _id: "monitor1" } }, true);
expect(loggerMock.error.calledOnce).to.be.true;
expect(loggerMock.error.calledWith("Null Monitor: monitor1")).to.be.true;
expect(loggerMock.error.firstCall.args[0].message).to.equal("Database error");
});
it("should update monitor status if different", async function () {
@@ -196,7 +190,7 @@ describe("NetworkService - handleStatusUpdate", function () {
await networkService.handleStatusUpdate({ data: { _id: "monitor1" } }, true);
expect(loggerMock.error.calledOnce).to.be.true;
expect(loggerMock.error.calledWith("Save error")).to.be.true;
expect(loggerMock.error.firstCall.args[0].message).to.equal("Save error");
});
});
@@ -308,43 +302,36 @@ describe("networkService - handlePing", () => {
sinon.restore();
});
it("should handle a successful ping response", async function () {
it("should handle a successful ping response", async () => {
const response = { alive: true };
const responseTime = 0;
pingMock.promise.probe.resolves(response);
pingMock.promise.probe.returns(response);
logAndStoreCheckStub.resolves();
await networkService.handlePing(job);
expect(
logAndStoreCheckStub.calledOnceWith(
{
monitorId: job.data._id,
status: true,
responseTime,
message: successMessages.PING_SUCCESS,
},
networkService.db.createCheck
)
logAndStoreCheckStub.calledOnceWith({
monitorId: job.data._id,
status: response.alive,
responseTime,
message: successMessages.PING_SUCCESS,
})
).to.be.true;
expect(handleStatusUpdateStub.calledOnceWith(job, true)).to.be.true;
});
it("should handle a successful ping response and isAlive === false", async function () {
it("should handle a successful response and isAlive === false", async function () {
const response = { alive: false };
const responseTime = 0;
pingMock.promise.probe.resolves(response);
logAndStoreCheckStub.resolves();
await networkService.handlePing(job);
console.log(logAndStoreCheckStub.getCall(0).args[0]);
expect(
logAndStoreCheckStub.calledOnceWith(
{
monitorId: job.data._id,
status: false,
responseTime,
message: errorMessages.PING_CANNOT_RESOLVE,
},
networkService.db.createCheck
)
logAndStoreCheckStub.calledOnceWith({
monitorId: job.data._id,
status: false,
responseTime,
message: errorMessages.PING_CANNOT_RESOLVE,
})
).to.be.true;
expect(handleStatusUpdateStub.calledOnceWith(job, false)).to.be.true;
});
@@ -625,6 +612,105 @@ describe("networkService - handlePagespeed", () => {
});
});
describe("networkService - handleHardware", () => {
let dbMock,
axiosMock,
jobMock,
emailServiceMock,
pingMock,
loggerMock,
httpMock,
networkService,
logAndStoreCheckStub,
handleStatusUpdateStub;
beforeEach(() => {
jobMock = {
data: {
_id: "12345",
url: "http://example.com",
},
};
dbMock = { getMonitorById: sinon.stub() };
axiosMock = { get: sinon.stub() };
emailServiceMock = sinon.stub();
pingMock = { promise: { probe: sinon.stub() } };
loggerMock = { error: sinon.stub() };
httpMock = {
STATUS_CODES: {
200: "OK",
500: "Internal Server Error",
},
};
networkService = new NetworkService(
dbMock,
emailServiceMock,
axiosMock,
pingMock,
loggerMock,
httpMock
);
logAndStoreCheckStub = sinon.stub(networkService, "logAndStoreCheck").resolves();
handleStatusUpdateStub = sinon.stub(networkService, "handleStatusUpdate").resolves();
});
afterEach(() => {
sinon.restore();
});
it("should handle a successful Hardware response", async () => {
const responseMock = {
monitorId: jobMock.data._id,
cpu: {
physical_core: 1,
logical_core: 1,
frequency: 266,
temperature: null,
free_percent: null,
usage_percent: null,
},
memory: {
total_bytes: 4,
available_bytes: 4,
used_bytes: 2,
usage_percent: 0.5,
},
disk: [
{
read_speed_bytes: 3,
write_speed_bytes: 3,
total_bytes: 10,
free_bytes: 2,
usage_percent: 0.8,
},
],
host: {
os: "Linux",
platform: "Ubuntu",
kernel_version: "24.04",
},
};
axiosMock.get.resolves(responseMock);
await networkService.handleHardware(jobMock);
expect(networkService.logAndStoreCheck.calledOnce).to.be.true;
const hardwareData = networkService.logAndStoreCheck.getCall(0).args[0];
expect(hardwareData.cpu).to.include({
...responseMock.cpu,
});
expect(networkService.handleStatusUpdate.calledOnceWith(jobMock, true)).to.be.true;
});
it("should handle an error Hardware response", async () => {
logAndStoreCheckStub.throws(new Error("Hardware error"));
try {
await networkService.handleHardware(jobMock);
} catch (error) {
expect(error.message).to.equal("Hardware error");
}
});
});
describe("NetworkService - getStatus", () => {
let dbMock, emailServiceMock, axiosMock, pingMock, loggerMock, httpMock, networkService;
@@ -685,12 +771,26 @@ describe("NetworkService - getStatus", () => {
const result = await networkService.getStatus(job);
expect(result).to.be.false;
});
it("should return true if the job type is hardware and handleHardware is successful", async () => {
const job = { data: { type: networkService.TYPE_HARDWARE } };
sinon.stub(networkService, "handleHardware").resolves(true);
const result = await networkService.getStatus(job);
expect(result).to.be.true;
});
it("should return false if the job type is hardware and handleHardware is not successful", async () => {
const job = { data: { type: networkService.TYPE_HARDWARE } };
sinon.stub(networkService, "handleHardware").resolves(false);
const result = await networkService.getStatus(job);
expect(result).to.be.false;
});
it("should log an error and return false if the job type is unknown", async () => {
const job = { data: { type: "unknown" } };
const result = await networkService.getStatus(job);
expect(result).to.be.false;
expect(loggerMock.error.calledOnce).to.be.true;
expect(loggerMock.error.calledWith(`Unsupported type: unknown`)).to.be.true;
expect(loggerMock.error.firstCall.args[0].message).to.equal(
"Unsupported type: unknown"
);
});
});
@@ -719,12 +819,14 @@ describe("NetworkService - logAndStoreCheck", async () => {
sinon.restore();
});
it("should log an error if `writeToDb` throws an error", async () => {
it("should log an error if writeToDb throws an error", async () => {
writeToDB.rejects(new Error("Database error"));
await networkService.logAndStoreCheck(data, writeToDB);
expect(loggerMock.error.calledOnce).to.be.true;
expect(loggerMock.error.calledWith(`Error writing check for ${data.monitorId}`)).to.be
.true;
expect(loggerMock.error.firstCall.args[0].details).to.equal(
`Error writing check for ${data.monitorId}`
);
});
it("should return the status of the inserted check if successful", async () => {
@@ -737,14 +839,17 @@ describe("NetworkService - logAndStoreCheck", async () => {
writeToDB.resolves(null);
await networkService.logAndStoreCheck(data, writeToDB);
expect(loggerMock.error.calledOnce).to.be.true;
expect(loggerMock.error.calledWith(`Error writing check for ${data.monitorId}`)).to.be
.true;
expect(loggerMock.error.firstCall.args[0].details).to.equal(
`Error writing check for ${data.monitorId}`
);
});
it("should thrown an error if the check is not inserted (undefined)", async () => {
writeToDB.resolves(undefined);
await networkService.logAndStoreCheck(data, writeToDB);
expect(loggerMock.error.calledOnce).to.be.true;
expect(loggerMock.error.calledWith(`Error writing check for ${data.monitorId}`)).to.be
.true;
expect(loggerMock.error.firstCall.args[0].details).to.equal(
`Error writing check for ${data.monitorId}`
);
});
});
@@ -0,0 +1,133 @@
import sinon from "sinon";
import SettingsService from "../../service/settingsService.js";
import { expect } from "chai";
import NetworkService from "../../service/networkService.js";
const SERVICE_NAME = "SettingsService";
describe("SettingsService", () => {
let sandbox, mockAppSettings;
beforeEach(function () {
sandbox = sinon.createSandbox();
sandbox.stub(process.env, "CLIENT_HOST").value("http://localhost");
sandbox.stub(process.env, "JWT_SECRET").value("secret");
sandbox.stub(process.env, "REFRESH_TOKEN_SECRET").value("refreshSecret");
sandbox.stub(process.env, "DB_TYPE").value("postgres");
sandbox
.stub(process.env, "DB_CONNECTION_STRING")
.value("postgres://user:pass@localhost/db");
sandbox.stub(process.env, "REDIS_HOST").value("localhost");
sandbox.stub(process.env, "REDIS_PORT").value("6379");
sandbox.stub(process.env, "TOKEN_TTL").value("3600");
sandbox.stub(process.env, "REFRESH_TOKEN_TTL").value("86400");
sandbox.stub(process.env, "PAGESPEED_API_KEY").value("apiKey");
sandbox.stub(process.env, "SYSTEM_EMAIL_HOST").value("smtp.mailtrap.io");
sandbox.stub(process.env, "SYSTEM_EMAIL_PORT").value("2525");
sandbox.stub(process.env, "SYSTEM_EMAIL_ADDRESS").value("test@example.com");
sandbox.stub(process.env, "SYSTEM_EMAIL_PASSWORD").value("password");
});
mockAppSettings = {
settingOne: 123,
settingTwo: 456,
};
afterEach(function () {
sandbox.restore();
sinon.restore();
});
describe("constructor", () => {
it("should construct a new SettingsService", () => {
const settingsService = new SettingsService(mockAppSettings);
expect(settingsService.appSettings).to.equal(mockAppSettings);
});
});
describe("loadSettings", () => {
it("should load settings from DB when environment variables are not set", async () => {
const dbSettings = { logLevel: "debug", apiBaseUrl: "http://localhost" };
const appSettings = { findOne: sinon.stub().returns(dbSettings) };
const settingsService = new SettingsService(appSettings);
settingsService.settings = {};
const result = await settingsService.loadSettings();
expect(result).to.deep.equal(dbSettings);
});
it("should throw an error if settings are not found", async function () {
const appSettings = { findOne: sinon.stub().returns(null) };
const settingsService = new SettingsService(appSettings);
settingsService.settings = null;
try {
await settingsService.loadSettings();
} catch (error) {
expect(error.message).to.equal("Settings not found");
expect(error.service).to.equal(SERVICE_NAME);
expect(error.method).to.equal("loadSettings");
}
});
it("should add its method and service name to error if not present", async () => {
const appSettings = { findOne: sinon.stub().throws(new Error("Test error")) };
const settingsService = new SettingsService(appSettings);
try {
await settingsService.loadSettings();
} catch (error) {
expect(error.message).to.equal("Test error");
expect(error.service).to.equal(SERVICE_NAME);
expect(error.method).to.equal("loadSettings");
}
});
it("should not add its method and service name to error if present", async () => {
const error = new Error("Test error");
error.method = "otherMethod";
error.service = "OTHER_SERVICE";
const appSettings = { findOne: sinon.stub().throws(error) };
const settingsService = new SettingsService(appSettings);
try {
await settingsService.loadSettings();
} catch (error) {
expect(error.message).to.equal("Test error");
expect(error.service).to.equal("OTHER_SERVICE");
expect(error.method).to.equal("otherMethod");
}
});
it("should merge DB settings with environment variables", async function () {
const dbSettings = { logLevel: "debug", apiBaseUrl: "http://localhost" };
const appSettings = { findOne: sinon.stub().returns(dbSettings) };
const settingsService = new SettingsService(appSettings);
const result = await settingsService.loadSettings();
expect(result).to.deep.equal(settingsService.settings);
expect(settingsService.settings.logLevel).to.equal("debug");
expect(settingsService.settings.apiBaseUrl).to.equal("http://localhost");
});
});
describe("reloadSettings", () => {
it("should call loadSettings", async () => {
const dbSettings = { logLevel: "debug", apiBaseUrl: "http://localhost" };
const appSettings = { findOne: sinon.stub().returns(dbSettings) };
const settingsService = new SettingsService(appSettings);
settingsService.settings = {};
const result = await settingsService.reloadSettings();
expect(result).to.deep.equal(dbSettings);
});
});
describe("getSettings", () => {
it("should return the current settings", () => {
const dbSettings = { logLevel: "debug", apiBaseUrl: "http://localhost" };
const appSettings = { findOne: sinon.stub().returns(dbSettings) };
const settingsService = new SettingsService(appSettings);
settingsService.settings = dbSettings;
const result = settingsService.getSettings();
expect(result).to.deep.equal(dbSettings);
});
it("should throw an error if settings have not been loaded", () => {
const appSettings = { findOne: sinon.stub().returns(null) };
const settingsService = new SettingsService(appSettings);
settingsService.settings = null;
try {
settingsService.getSettings();
} catch (error) {
expect(error.message).to.equal("Settings have not been loaded");
}
});
});
});
+118
View File
@@ -0,0 +1,118 @@
import sinon from "sinon";
import logger from "../../utils/logger.js";
import { Logger } from "../../utils/logger.js";
import winston from "winston";
describe("Logger", () => {
let infoStub, warnStub, errorStub;
beforeEach(() => {
infoStub = sinon.stub(logger.logger, "info");
warnStub = sinon.stub(logger.logger, "warn");
errorStub = sinon.stub(logger.logger, "error");
});
afterEach(() => {
sinon.restore();
});
describe("constructor", () => {
let createLoggerStub;
beforeEach(function () {
createLoggerStub = sinon.stub(winston, "createLogger");
});
afterEach(function () {
sinon.restore();
});
it("should convert message to JSON string if it is an object", function () {
const logMessage = { key: "value" };
const expectedMessage = JSON.stringify(logMessage, null, 2);
createLoggerStub.callsFake((config) => {
const consoleTransport = config.transports[0];
const logEntry = {
level: "info",
message: logMessage,
timestamp: new Date().toISOString(),
};
const formattedMessage = consoleTransport.format.transform(logEntry);
expect(formattedMessage).to.include(expectedMessage);
return { log: sinon.spy() };
});
const logger = new Logger();
logger.logger.info(logMessage);
});
});
describe("info", () => {
it("should log an informational message", () => {
const config = {
message: "Info message",
service: "TestService",
method: "TestMethod",
details: { key: "value" },
};
logger.info(config);
expect(infoStub.calledOnce).to.be.true;
expect(
infoStub.calledWith(config.message, {
service: config.service,
method: config.method,
details: config.details,
})
).to.be.true;
});
});
describe("warn", () => {
it("should log a warning message", () => {
const config = {
message: "Warning message",
service: "TestService",
method: "TestMethod",
details: { key: "value" },
};
logger.warn(config);
expect(warnStub.calledOnce).to.be.true;
expect(
warnStub.calledWith(config.message, {
service: config.service,
method: config.method,
details: config.details,
})
).to.be.true;
});
});
describe("error", () => {
it("should log an error message", () => {
const config = {
message: "Error message",
service: "TestService",
method: "TestMethod",
details: { key: "value" },
stack: "Error stack trace",
};
logger.error(config);
expect(errorStub.calledOnce).to.be.true;
expect(
errorStub.calledWith(config.message, {
service: config.service,
method: config.method,
details: config.details,
stack: config.stack,
})
).to.be.true;
});
});
});
+89 -21
View File
@@ -1,23 +1,91 @@
import winston from "winston";
/**
* @module
* @example
* logger.info("Registered a new user!")
* logger.warn("User not found!")
* logger.error("Cannot save")
* @example
* "Specify service and ID in the log if applicable."
* logger.error("Descriptive Message",{"service":"monitor","monitorId":"123456"})
* logger.error("Incorrect Credentials",{"service":"Auth","userId":"654321"})
* logger.error("User not found!",{"service":"Auth"})
*/
const logger = winston.createLogger({
level: "info",
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: "app.log" }),
],
});
import { createLogger, format, transports } from "winston";
class Logger {
constructor() {
const consoleFormat = format.printf(
({ level, message, service, method, details, timestamp }) => {
if (message instanceof Object) {
message = JSON.stringify(message, null, 2);
}
let msg = `${timestamp} ${level}:`;
service && (msg += ` [${service}]`);
method && (msg += `(${method})`);
message && (msg += ` ${message}`);
details && (msg += ` (details: ${details})`);
return msg;
}
);
this.logger = createLogger({
level: "info",
format: format.combine(format.timestamp()),
transports: [
new transports.Console({
format: format.combine(
format.colorize(),
format.prettyPrint(),
format.json(),
consoleFormat
),
}),
new transports.File({
format: format.combine(format.json()),
filename: "app.log",
}),
],
});
}
/**
* Logs an informational message.
* @param {Object} config - The configuration object.
* @param {string} config.message - The message to log.
* @param {string} config.service - The service name.
* @param {string} config.method - The method name.
* @param {Object} config.details - Additional details.
*/
info(config) {
this.logger.info(config.message, {
service: config.service,
method: config.method,
details: config.details,
});
}
/**
* Logs a warning message.
* @param {Object} config - The configuration object.
* @param {string} config.message - The message to log.
* @param {string} config.service - The service name.
* @param {string} config.method - The method name.
* @param {Object} config.details - Additional details.
*/
warn(config) {
this.logger.warn(config.message, {
service: config.service,
method: config.method,
details: config.details,
});
}
/**
* Logs an error message.
* @param {Object} config - The configuration object.
* @param {string} config.message - The message to log.
* @param {string} config.service - The service name.
* @param {string} config.method - The method name.
* @param {Object} config.details - Additional details.
*/
error(config) {
this.logger.error(config.message, {
service: config.service,
method: config.method,
details: config.details,
stack: config.stack,
});
}
}
const logger = new Logger();
export { Logger };
export default logger;
+1 -1
View File
@@ -62,7 +62,6 @@ const successMessages = {
ALERT_EDIT: "Alert edited successfully",
ALERT_DELETE: "Alert deleted successfully",
// Auth Controller
AUTH_CREATE_USER: "User created successfully",
AUTH_LOGIN_USER: "User logged in successfully",
AUTH_LOGOUT_USER: "User logged out successfully",
@@ -72,6 +71,7 @@ const successMessages = {
AUTH_RESET_PASSWORD: "Password reset successfully",
AUTH_ADMIN_CHECK: "Admin check completed successfully",
AUTH_DELETE_USER: "User deleted successfully",
AUTH_TOKEN_REFRESHED: "Auth token is refreshed",
// Check Controller
CHECK_CREATE: "Check created successfully",
+6
View File
@@ -0,0 +1,6 @@
{
"name": "bluewave-uptime",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
+6
View File
@@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
]
}