mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-05-02 22:49:19 -05:00
Merge branch 'develop' into feat/be/job-queue-tests
This commit is contained in:
@@ -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",
|
||||
},
|
||||
};
|
||||
|
||||
Generated
+30
-30
@@ -20,8 +20,8 @@
|
||||
"@reduxjs/toolkit": "2.2.5",
|
||||
"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",
|
||||
@@ -29,7 +29,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"
|
||||
},
|
||||
@@ -2304,9 +2304,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
|
||||
"integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
|
||||
"version": "18.3.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.11.tgz",
|
||||
"integrity": "sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
@@ -2314,9 +2314,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "18.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz",
|
||||
"integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==",
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2606,9 +2606,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
|
||||
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
|
||||
"version": "1.7.7",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
|
||||
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
@@ -2766,9 +2766,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/chart.js": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.3.tgz",
|
||||
"integrity": "sha512-qK1gkGSRYcJzqrrzdR6a+I0vQ4/R+SoODXyAjscQ/4mzuNzySaMCd+hyVxitSY1+L2fjPD1Gbn+ibNqRmwQeLw==",
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.5.tgz",
|
||||
"integrity": "sha512-CVVjg1RYTJV9OCC8WeJPMx8gsV8K6WIyIEQUE3ui4AR9Hfgls9URri6Ja3hyMVBbTF8Q2KFa19PE815gWcWhng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@kurkle/color": "^0.3.0"
|
||||
@@ -3051,9 +3051,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.11",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz",
|
||||
"integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==",
|
||||
"version": "1.11.13",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
@@ -4686,9 +4686,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/joi": {
|
||||
"version": "17.13.1",
|
||||
"resolved": "https://registry.npmjs.org/joi/-/joi-17.13.1.tgz",
|
||||
"integrity": "sha512-vaBlIKCyo4FCUtCm7Eu4QZd/q02bWcxfUO6YSXAZOWF6gzcLBeba8kwotUdYJjDLW8Cz8RywsSOqiNJZW0mNvg==",
|
||||
"version": "17.13.3",
|
||||
"resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
|
||||
"integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@hapi/hoek": "^9.3.0",
|
||||
@@ -5448,9 +5448,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-toastify": {
|
||||
"version": "10.0.5",
|
||||
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.5.tgz",
|
||||
"integrity": "sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==",
|
||||
"version": "10.0.6",
|
||||
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.6.tgz",
|
||||
"integrity": "sha512-yYjp+omCDf9lhZcrZHKbSq7YMuK0zcYkDFTzfRFgTXkTFHZ1ToxwAonzA4JI5CxA91JpjFLmwEsZEgfYfOqI1A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.0"
|
||||
@@ -5477,9 +5477,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/recharts": {
|
||||
"version": "2.13.0-alpha.4",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.13.0-alpha.4.tgz",
|
||||
"integrity": "sha512-K9naL6F7pEcDYJE6yFQASSCQecSLPP0JagnvQ9hPtA/aHgsxsnIOjouLP5yrFZehxzfCkV5TEORr7/uNtSr7Qw==",
|
||||
"version": "2.13.0",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.13.0.tgz",
|
||||
"integrity": "sha512-sbfxjWQ+oLWSZEWmvbq/DFVdeRLqqA6d0CDjKx2PkxVVdoXo16jvENCE+u/x7HxOO+/fwx//nYRwb8p8X6s/lQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clsx": "^2.0.0",
|
||||
@@ -6234,9 +6234,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.8",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz",
|
||||
"integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==",
|
||||
"version": "5.4.9",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.9.tgz",
|
||||
"integrity": "sha512-20OVpJHh0PAM0oSOELa5GaZNWeDjcAvQjGXy2Uyr+Tp+/D2/Hdz6NLgpJLsarPTA2QJ6v8mX2P1ZfbsSKvdMkg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
|
||||
+3
-3
@@ -22,8 +22,8 @@
|
||||
"@reduxjs/toolkit": "2.2.5",
|
||||
"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"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
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}
|
||||
>
|
||||
<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 };
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,47 +12,45 @@ import { useNavigate } from "react-router";
|
||||
import { getAppSettings, updateAppSettings } from "../../Features/Settings/settingsSlice";
|
||||
import { useState, useEffect } from "react";
|
||||
import Select from "../../Components/Inputs/Select";
|
||||
import { advancedSettingsValidation } from "../../Validation/validation";
|
||||
import { buildErrors, hasValidationErrors } from "../../Validation/error";
|
||||
|
||||
const AdvancedSettings = ({ isAdmin }) => {
|
||||
const navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
if (!isAdmin) {
|
||||
navigate("/");
|
||||
}
|
||||
}, [navigate, isAdmin]);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const theme = useTheme();
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const dispatch = useDispatch();
|
||||
const settings = useSelector((state) => state.settings);
|
||||
const [localSettings, setLocalSettings] = useState({
|
||||
apiBaseUrl: "",
|
||||
logLevel: "debug",
|
||||
systemEmailHost: "",
|
||||
systemEmailPort: "",
|
||||
systemEmailAddress: "",
|
||||
systemEmailPassword: "",
|
||||
jwtTTL: "",
|
||||
dbType: "",
|
||||
redisHost: "",
|
||||
redisPort: "",
|
||||
pagespeedApiKey: "",
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
useEffect(() => {
|
||||
if (!isAdmin) {
|
||||
navigate("/");
|
||||
}
|
||||
}, [navigate, isAdmin]);
|
||||
|
||||
useEffect(() => {
|
||||
const getSettings = async () => {
|
||||
const action = await dispatch(getAppSettings({ authToken }));
|
||||
if (action.payload?.success) {
|
||||
setLocalSettings(action.payload.data);
|
||||
} else {
|
||||
createToast({ body: "Failed to get settings" });
|
||||
}
|
||||
};
|
||||
getSettings();
|
||||
}, [authToken, dispatch]);
|
||||
const theme = useTheme();
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const dispatch = useDispatch();
|
||||
const settings = useSelector((state) => state.settings);
|
||||
const [localSettings, setLocalSettings] = useState({
|
||||
apiBaseUrl: "",
|
||||
logLevel: "debug",
|
||||
systemEmailHost: "",
|
||||
systemEmailPort: "",
|
||||
systemEmailAddress: "",
|
||||
systemEmailPassword: "",
|
||||
jwtTTL: "",
|
||||
dbType: "",
|
||||
redisHost: "",
|
||||
redisPort: "",
|
||||
pagespeedApiKey: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const getSettings = async () => {
|
||||
const action = await dispatch(getAppSettings({ authToken }));
|
||||
if (action.payload.success) {
|
||||
setLocalSettings(action.payload.data);
|
||||
} else {
|
||||
createToast({ body: "Failed to get settings" });
|
||||
}
|
||||
};
|
||||
getSettings();
|
||||
}, [authToken, dispatch]);
|
||||
|
||||
const logItems = [
|
||||
{ _id: 1, name: "none" },
|
||||
@@ -74,210 +72,189 @@ const AdvancedSettings = ({ isAdmin }) => {
|
||||
setLocalSettings({ ...localSettings, logLevel: newLogLevel });
|
||||
};
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
setLocalSettings({ ...localSettings, [id]: value });
|
||||
const { error } = advancedSettingsValidation.validate(
|
||||
{ [id]: value },
|
||||
{
|
||||
abortEarly: false,
|
||||
}
|
||||
);
|
||||
setErrors((prev) => {
|
||||
return buildErrors(prev, id, error);
|
||||
});
|
||||
};
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
setLocalSettings({ ...localSettings, [id]: value });
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (hasValidationErrors(localSettings, advancedSettingsValidation, setErrors))
|
||||
return;
|
||||
const action = await dispatch(
|
||||
updateAppSettings({ settings: localSettings, authToken })
|
||||
);
|
||||
let body = "";
|
||||
if (action.payload.success) {
|
||||
console.log(action.payload.data);
|
||||
setLocalSettings(action.payload.data);
|
||||
body = "Settings saved successfully";
|
||||
} else {
|
||||
body = "Failed to save settings";
|
||||
}
|
||||
createToast({ body });
|
||||
};
|
||||
const handleSave = async () => {
|
||||
const action = await dispatch(
|
||||
updateAppSettings({ settings: localSettings, authToken })
|
||||
);
|
||||
let body = "";
|
||||
if (action.payload.success) {
|
||||
console.log(action.payload.data);
|
||||
setLocalSettings(action.payload.data);
|
||||
body = "Settings saved successfully";
|
||||
} else {
|
||||
body = "Failed to save settings";
|
||||
}
|
||||
createToast({ body });
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="settings"
|
||||
style={{
|
||||
paddingBottom: 0,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
component="form"
|
||||
gap={theme.spacing(12)}
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">Client settings</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Modify client settings here.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Field
|
||||
id="apiBaseUrl"
|
||||
label="API URL Host"
|
||||
value={localSettings.apiBaseUrl}
|
||||
onChange={handleChange}
|
||||
error={errors.apiBaseUrl}
|
||||
/>
|
||||
<Select
|
||||
id="logLevel"
|
||||
label="Log level"
|
||||
name="logLevel"
|
||||
items={logItems}
|
||||
value={logItemLookup[localSettings.logLevel]}
|
||||
onChange={handleLogLevel}
|
||||
error={errors.logLevel}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">Email settings</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Set your host email settings here. These settings are used for
|
||||
sending system emails.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Field
|
||||
type="text"
|
||||
id="systemEmailHost"
|
||||
label="Email host"
|
||||
name="systemEmailHost"
|
||||
value={localSettings.systemEmailHost}
|
||||
onChange={handleChange}
|
||||
error={errors.systemEmailHost}
|
||||
/>
|
||||
<Field
|
||||
type="number"
|
||||
id="systemEmailPort"
|
||||
label="System email address"
|
||||
name="systemEmailPort"
|
||||
value={localSettings.systemEmailPort.toString()}
|
||||
onChange={handleChange}
|
||||
error={errors.systemEmailPort}
|
||||
/>
|
||||
<Field
|
||||
type="email"
|
||||
id="systemEmailAddress"
|
||||
label="System email address"
|
||||
name="systemEmailAddress"
|
||||
value={localSettings.systemEmailAddress}
|
||||
onChange={handleChange}
|
||||
error={errors.systemEmailAddress}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="systemEmailPassword"
|
||||
label="System email password"
|
||||
name="systemEmailPassword"
|
||||
value={localSettings.systemEmailPassword}
|
||||
onChange={handleChange}
|
||||
error={errors.systemEmailPassword}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">Server settings</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Modify server settings here.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Field
|
||||
type="text"
|
||||
id="jwtTTL"
|
||||
label="JWT time to live"
|
||||
name="jwtTTL"
|
||||
value={localSettings.jwtTTL}
|
||||
onChange={handleChange}
|
||||
error={errors.jwtTTL}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="dbType"
|
||||
label="Database type"
|
||||
name="dbType"
|
||||
value={localSettings.dbType}
|
||||
onChange={handleChange}
|
||||
error={errors.dbType}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="redisHost"
|
||||
label="Redis host"
|
||||
name="redisHost"
|
||||
value={localSettings.redisHost}
|
||||
onChange={handleChange}
|
||||
error={errors.redisHost}
|
||||
/>
|
||||
<Field
|
||||
type="number"
|
||||
id="redisPort"
|
||||
label="Redis port"
|
||||
name="redisPort"
|
||||
value={localSettings.redisPort.toString()}
|
||||
onChange={handleChange}
|
||||
error={errors.redisPort}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="pagespeedApiKey"
|
||||
label="PageSpeed API key"
|
||||
name="pagespeedApiKey"
|
||||
value={localSettings.pagespeedApiKey}
|
||||
onChange={handleChange}
|
||||
error={errors.pagespeedApiKey}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">About</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography component="h2">BlueWave Uptime v1.0.0</Typography>
|
||||
<Typography
|
||||
sx={{ mt: theme.spacing(2), mb: theme.spacing(6), opacity: 0.6 }}
|
||||
>
|
||||
Developed by Bluewave Labs.
|
||||
</Typography>
|
||||
<Link
|
||||
level="secondary"
|
||||
url="https://github.com/bluewave-labs"
|
||||
label="https://github.com/bluewave-labs"
|
||||
/>
|
||||
</Box>
|
||||
</ConfigBox>
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<LoadingButton
|
||||
loading={settings.isLoading || settings.authIsLoading}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
return (
|
||||
<Box
|
||||
className="settings"
|
||||
style={{
|
||||
paddingBottom: 0,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
component="form"
|
||||
gap={theme.spacing(12)}
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">Client settings</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Modify client settings here.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Field
|
||||
id="apiBaseUrl"
|
||||
label="API URL Host"
|
||||
value={localSettings.apiBaseUrl}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Select
|
||||
id="logLevel"
|
||||
label="Log level"
|
||||
name="logLevel"
|
||||
items={logItems}
|
||||
value={logItemLookup[localSettings.logLevel]}
|
||||
onChange={handleLogLevel}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">Email settings</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Set your host email settings here. These settings are used for sending
|
||||
system emails.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Field
|
||||
type="text"
|
||||
id="systemEmailHost"
|
||||
label="Email host"
|
||||
name="systemEmailHost"
|
||||
value={localSettings.systemEmailHost}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="number"
|
||||
id="systemEmailPort"
|
||||
label="System email address"
|
||||
name="systemEmailPort"
|
||||
value={localSettings.systemEmailPort.toString()}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="email"
|
||||
id="systemEmailAddress"
|
||||
label="System email address"
|
||||
name="systemEmailAddress"
|
||||
value={localSettings.systemEmailAddress}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="systemEmailPassword"
|
||||
label="System email password"
|
||||
name="systemEmailPassword"
|
||||
value={localSettings.systemEmailPassword}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">Server settings</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Modify server settings here.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Field
|
||||
type="text"
|
||||
id="jwtTTL"
|
||||
label="JWT time to live"
|
||||
name="jwtTTL"
|
||||
value={localSettings.jwtTTL}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="dbType"
|
||||
label="Database type"
|
||||
name="dbType"
|
||||
value={localSettings.dbType}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="redisHost"
|
||||
label="Redis host"
|
||||
name="redisHost"
|
||||
value={localSettings.redisHost}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="number"
|
||||
id="redisPort"
|
||||
label="Redis port"
|
||||
name="redisPort"
|
||||
value={localSettings.redisPort.toString()}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="pagespeedApiKey"
|
||||
label="PageSpeed API key"
|
||||
name="pagespeedApiKey"
|
||||
value={localSettings.pagespeedApiKey}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">About</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography component="h2">BlueWave Uptime v1.0.0</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(6), opacity: 0.6 }}>
|
||||
Developed by Bluewave Labs.
|
||||
</Typography>
|
||||
<Link
|
||||
level="secondary"
|
||||
url="https://github.com/bluewave-labs"
|
||||
label="https://github.com/bluewave-labs"
|
||||
/>
|
||||
</Box>
|
||||
</ConfigBox>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<LoadingButton
|
||||
loading={settings.isLoading || settings.authIsLoading}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
AdvancedSettings.propTypes = {
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
MS_PER_WEEK,
|
||||
} from "../../../Utils/timeUtils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { buildErrors, hasValidationErrors } from "../../../Validation/error";
|
||||
|
||||
const getDurationAndUnit = (durationInMs) => {
|
||||
if (durationInMs % MS_PER_DAY === 0) {
|
||||
@@ -146,36 +145,46 @@ const CreateMaintenance = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await networkService.getMaintenanceWindowById({
|
||||
authToken: authToken,
|
||||
maintenanceWindowId: maintenanceWindowId,
|
||||
});
|
||||
const maintenanceWindow = res.data.data;
|
||||
const { name, start, end, repeat, monitorId } = maintenanceWindow;
|
||||
const startTime = dayjs(start);
|
||||
const endTime = dayjs(end);
|
||||
const durationInMs = endTime.diff(startTime, "milliseconds").toString();
|
||||
const { duration, durationUnit } = getDurationAndUnit(durationInMs);
|
||||
const monitor = monitors.find((monitor) => monitor._id === monitorId);
|
||||
setForm({
|
||||
...form,
|
||||
name,
|
||||
repeat: REVERSE_REPEAT_LOOKUP[repeat],
|
||||
startDate: startTime,
|
||||
startTime,
|
||||
duration,
|
||||
durationUnit,
|
||||
monitors: monitor ? [monitor] : [],
|
||||
});
|
||||
} catch (error) {
|
||||
createToast({ body: "Failed to fetch data" });
|
||||
logger.error("Failed to fetch monitors", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchMonitors();
|
||||
}, [authToken, user]);
|
||||
const res = await networkService.getMaintenanceWindowById({
|
||||
authToken: authToken,
|
||||
maintenanceWindowId: maintenanceWindowId,
|
||||
});
|
||||
const maintenanceWindow = res.data.data;
|
||||
const { name, start, end, repeat, monitorId } = maintenanceWindow;
|
||||
const startTime = dayjs(start);
|
||||
const endTime = dayjs(end);
|
||||
const durationInMs = endTime.diff(startTime, "milliseconds").toString();
|
||||
const { duration, durationUnit } = getDurationAndUnit(durationInMs);
|
||||
const monitor = monitors.find((monitor) => monitor._id === monitorId);
|
||||
setForm({
|
||||
...form,
|
||||
name,
|
||||
repeat: REVERSE_REPEAT_LOOKUP[repeat],
|
||||
startDate: startTime,
|
||||
startTime,
|
||||
duration,
|
||||
durationUnit,
|
||||
monitors: monitor ? [monitor] : [],
|
||||
});
|
||||
} catch (error) {
|
||||
createToast({ body: "Failed to fetch data" });
|
||||
logger.error("Failed to fetch monitors", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchMonitors();
|
||||
}, [authToken, user]);
|
||||
|
||||
const buildErrors = (prev, id, error) => {
|
||||
const updatedErrors = { ...prev };
|
||||
if (error) {
|
||||
updatedErrors[id] = error.details[0].message;
|
||||
} else {
|
||||
delete updatedErrors[id];
|
||||
}
|
||||
return updatedErrors;
|
||||
};
|
||||
|
||||
const handleSearch = (value) => {
|
||||
setSearch(value);
|
||||
@@ -214,17 +223,29 @@ const CreateMaintenance = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if(hasValidationErrors(form, maintenanceWindowValidation, setErrors))
|
||||
return;
|
||||
// Build timestamp for maintenance window from startDate and startTime
|
||||
const start = dayjs(form.startDate)
|
||||
.set("hour", form.startTime.hour())
|
||||
.set("minute", form.startTime.minute());
|
||||
// Build end timestamp for maintenance window
|
||||
const MS_MULTIPLIER = MS_LOOKUP[form.durationUnit];
|
||||
const durationInMs = form.duration * MS_MULTIPLIER;
|
||||
const end = start.add(durationInMs);
|
||||
const handleSubmit = async () => {
|
||||
const { error } = maintenanceWindowValidation.validate(form, {
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
// If errors, return early
|
||||
if (error) {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
logger.error(error);
|
||||
return;
|
||||
}
|
||||
// Build timestamp for maintenance window from startDate and startTime
|
||||
const start = dayjs(form.startDate)
|
||||
.set("hour", form.startTime.hour())
|
||||
.set("minute", form.startTime.minute());
|
||||
// Build end timestamp for maintenance window
|
||||
const MS_MULTIPLIER = MS_LOOKUP[form.durationUnit];
|
||||
const durationInMs = form.duration * MS_MULTIPLIER;
|
||||
const end = start.add(durationInMs);
|
||||
|
||||
// Get repeat value in milliseconds
|
||||
const repeat = REPEAT_LOOKUP[form.repeat];
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
const buildErrors = (prev, id, error) => {
|
||||
const updatedErrors = { ...prev };
|
||||
if (error) {
|
||||
updatedErrors[id] = error.details[0].message?? "Validation error";
|
||||
} else {
|
||||
delete updatedErrors[id];
|
||||
}
|
||||
return updatedErrors;
|
||||
};
|
||||
|
||||
const hasValidationErrors = (form, validation, setErrors) => {
|
||||
const { error } = validation.validate(form, {
|
||||
abortEarly: false,
|
||||
});
|
||||
if (error) {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message?? "Validation error";
|
||||
});
|
||||
setErrors(newErrors);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
export { buildErrors, hasValidationErrors };
|
||||
@@ -128,43 +128,10 @@ const maintenanceWindowValidation = joi.object({
|
||||
monitors: joi.array().min(1),
|
||||
});
|
||||
|
||||
const advancedSettingsValidation = joi.object({
|
||||
apiBaseUrl: joi.string().uri({ allowRelative: true }).trim().messages({
|
||||
"string.empty": "API base url is required.",
|
||||
"string.uri": "The URL you provided is not valid.",
|
||||
}),
|
||||
logLevel: joi.string().valid("debug", "none", "error", "warn").allow(""),
|
||||
systemEmailHost: joi.string().allow(''),
|
||||
systemEmailPort: joi.string().allow(''),
|
||||
systemEmailAddress: joi
|
||||
.string().allow(''),
|
||||
systemEmailPassword: joi.string().allow(''),
|
||||
jwtTTL: joi
|
||||
.string()
|
||||
.trim()
|
||||
.messages({
|
||||
"string.empty": "JWT TTL is required."
|
||||
}),
|
||||
dbType: joi.string().trim().messages({
|
||||
"string.empty": "DB type is required",
|
||||
}),
|
||||
redisHost: joi.string().trim().messages({
|
||||
"string.empty": "Redis host is required",
|
||||
}),
|
||||
redisPort: joi.string().allow('').custom((value, helpers) => {
|
||||
if(value && isNaN(parseInt(value))){
|
||||
return helpers.message("Redit port must be a number")
|
||||
}
|
||||
return value
|
||||
}),
|
||||
pagespeedApiKey: joi.string().allow('')
|
||||
});
|
||||
|
||||
export {
|
||||
credentials,
|
||||
imageValidation,
|
||||
monitorValidation,
|
||||
settingsValidation,
|
||||
maintenanceWindowValidation,
|
||||
advancedSettingsValidation,
|
||||
};
|
||||
|
||||
@@ -173,6 +173,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
|
||||
@@ -455,6 +514,7 @@ export {
|
||||
issueToken,
|
||||
registerUser,
|
||||
loginUser,
|
||||
refreshAuthToken,
|
||||
editUser,
|
||||
checkSuperadminExists,
|
||||
requestRecovery,
|
||||
|
||||
+72
-1
@@ -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 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+461
-378
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -29,7 +29,7 @@
|
||||
"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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user