Merge remote-tracking branch 'upstream/master' into feat/details-pg
This commit is contained in:
commit
41b65c1f3b
13 files changed
+388
-420
No files matched your search
@@ -0,0 +1,9 @@
|
||||
.alert {
|
||||
margin: 0;
|
||||
width: fit-content;
|
||||
padding: var(--env-var-spacing-1-plus);
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.alert .MuiStack-root {
|
||||
max-width: 85%;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack } from "@mui/material";
|
||||
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
||||
import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined";
|
||||
import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined";
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
* Icons mapping for different alert variants.
|
||||
* @type {Object<string, JSX.Element>}
|
||||
*/
|
||||
|
||||
const icons = {
|
||||
info: <InfoOutlinedIcon />,
|
||||
error: <ErrorOutlineOutlinedIcon />,
|
||||
warning: <WarningAmberOutlinedIcon />,
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} props
|
||||
* @param {'info' | 'error' | 'warning'} props.variant - The type of alert.
|
||||
* @param {string} [props.title] - The title of the alert.
|
||||
* @param {string} [props.body] - The body text of the alert.
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
|
||||
const Alert = ({ variant, title, body }) => {
|
||||
const theme = useTheme();
|
||||
const { bg, border, color } = theme.alert[variant];
|
||||
const icon = icons[variant];
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
className="alert row-stack"
|
||||
gap={theme.gap.ml}
|
||||
sx={{
|
||||
backgroundColor: bg,
|
||||
border: `solid 1px ${border}`,
|
||||
borderRadius: `${theme.shape.borderRadius}px`,
|
||||
}}
|
||||
>
|
||||
{icon && <Box sx={{ color: color }}>{icon}</Box>}
|
||||
<Stack direction="column" gap="2px" sx={{ flex: 1, color: color }}>
|
||||
{title && <Box sx={{ fontWeight: "700" }}>{title}</Box>}
|
||||
{body && <Box sx={{ fontWeight: "400" }}>{body}</Box>}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Alert.propTypes = {
|
||||
variant: PropTypes.oneOf(["info", "error", "warning"]).isRequired,
|
||||
title: PropTypes.string,
|
||||
body: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Alert;
|
||||
@@ -1,14 +1,11 @@
|
||||
import TabPanel from "@mui/lab/TabPanel";
|
||||
import React, { useState } from "react";
|
||||
// import AnnouncementsDualButtonWithIcon from "../../Announcements/AnnouncementsDualButtonWithIcon/AnnouncementsDualButtonWithIcon";
|
||||
import AnnouncementsDualButtonWithIcon from "../../Toast/AnnouncementsDualButtonWithIcon/AnnouncementsDualButtonWithIcon";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Divider, Stack, Typography } from "@mui/material";
|
||||
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
||||
import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined";
|
||||
import ButtonSpinner from "../../ButtonSpinner";
|
||||
import PasswordTextField from "../../TextFields/Password/PasswordTextField";
|
||||
import { editPasswordValidation } from "../../../Validation/validation";
|
||||
import Alert from "../../Alert";
|
||||
|
||||
/**
|
||||
* PasswordPanel component manages the form for editing password.
|
||||
@@ -96,13 +93,9 @@ const PasswordPanel = () => {
|
||||
spellCheck="false"
|
||||
>
|
||||
<div className="edit-password-form__wrapper">
|
||||
<AnnouncementsDualButtonWithIcon
|
||||
icon={
|
||||
<InfoOutlinedIcon
|
||||
style={{ fill: theme.palette.secondary.main }}
|
||||
/>
|
||||
}
|
||||
subject="SSO login"
|
||||
<Alert
|
||||
variant="info"
|
||||
title="SSO login"
|
||||
body="Since you logged in via SSO, you cannot reset or modify your password."
|
||||
/>
|
||||
</div>
|
||||
@@ -181,8 +174,8 @@ const PasswordPanel = () => {
|
||||
<div className="edit-password-form__wrapper">
|
||||
<Stack></Stack>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<AnnouncementsDualButtonWithIcon
|
||||
icon={<WarningAmberOutlinedIcon style={{ fill: "#f79009" }} />}
|
||||
<Alert
|
||||
variant="warning"
|
||||
body="New password must contain at least 8 characters and must have at least one uppercase letter, one number and one symbol."
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -28,8 +28,7 @@
|
||||
margin: 50px 0;
|
||||
}
|
||||
.account .MuiDivider-root,
|
||||
.account .MuiOutlinedInput-root fieldset,
|
||||
.account .announcement-tile {
|
||||
.account .MuiOutlinedInput-root fieldset {
|
||||
border-color: var(--env-var-color-29);
|
||||
}
|
||||
.account [class$="-form"] {
|
||||
@@ -42,9 +41,9 @@
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.edit-profile-form__wrapper .MuiStack-root:not(.row-stack),
|
||||
.edit-password-form__wrapper .MuiStack-root:not(.row-stack),
|
||||
.edit-organization-form__wrapper .MuiStack-root:not(.row-stack) {
|
||||
.edit-profile-form__wrapper>.MuiStack-root:not(.row-stack),
|
||||
.edit-password-form__wrapper>.MuiStack-root:not(.row-stack),
|
||||
.edit-organization-form__wrapper>.MuiStack-root:not(.row-stack) {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-right: 10px;
|
||||
@@ -74,36 +73,13 @@
|
||||
#input-team-member {
|
||||
padding: 10px;
|
||||
}
|
||||
.edit-password-form__wrapper > .announcement-tile {
|
||||
width: 100%;
|
||||
}
|
||||
.edit-password-form__wrapper
|
||||
> .announcement-tile
|
||||
.announcement-content-subject {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
#edit-new-password + div > button > svg,
|
||||
#edit-confirm-password + div > button > svg,
|
||||
#edit-current-password + div > button > svg {
|
||||
color: var(--env-var-color-29);
|
||||
}
|
||||
.edit-password-form__wrapper .announcement-tile {
|
||||
margin: 0;
|
||||
}
|
||||
.edit-password-form__wrapper .announcement-tile .announcement-close {
|
||||
margin-left: auto;
|
||||
}
|
||||
.MuiBox-root > .announcement-tile {
|
||||
border-color: var(--env-var-color-27);
|
||||
background-color: var(--lighter-env-var-color-27);
|
||||
}
|
||||
.MuiBox-root > .announcement-tile .announcement-content-body {
|
||||
color: var(--env-var-color-28);
|
||||
}
|
||||
.MuiBox-root > .announcement-tile .announcement-close svg,
|
||||
.edit-password-form__wrapper > .announcement-tile .announcement-close svg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
#org-name-flex-container .Mui-disabled {
|
||||
color: var(--env-var-color-2) !important;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Tabs,
|
||||
Typography,
|
||||
Stack,
|
||||
Box,
|
||||
} from "@mui/material";
|
||||
import { ColoredLabel, StatusLabel } from "../../Components/Label/";
|
||||
import Avatar from "../../Components/Avatar/";
|
||||
@@ -29,7 +30,7 @@ import AddIcon from "@mui/icons-material/Add";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import UploadIcon from "@mui/icons-material/Upload";
|
||||
import SendIcon from "@mui/icons-material/Send";
|
||||
import ImageIcon from '@mui/icons-material/Image';
|
||||
import ImageIcon from "@mui/icons-material/Image";
|
||||
|
||||
// Redux
|
||||
import { useSelector } from "react-redux";
|
||||
@@ -38,6 +39,7 @@ import { getMonitors } from "../../Features/Monitors/monitorsSlice";
|
||||
import { getMonitorsByUserId } from "../../Features/Monitors/monitorsSlice";
|
||||
import ImageField from "../../Components/TextFields/Image";
|
||||
import ProgressUpload from "../../Components/ProgressBars";
|
||||
import Alert from "../../Components/Alert";
|
||||
|
||||
const cols = [
|
||||
{
|
||||
@@ -423,7 +425,30 @@ const Demo = () => {
|
||||
</Stack>
|
||||
<Divider sx={{ margin: `${theme.spacing(2)}` }} />
|
||||
<Stack justifyContent="center" alignItems="center">
|
||||
<ProgressUpload icon={<ImageIcon />} label="image.jpg" size="2 MB"/>
|
||||
<ProgressUpload icon={<ImageIcon />} label="image.jpg" size="2 MB" />
|
||||
</Stack>
|
||||
<Divider sx={{ margin: `${theme.spacing(2)}` }} />
|
||||
<Stack justifyContent="center" alignItems="center">
|
||||
<Box width="500px">
|
||||
<Alert
|
||||
variant="info"
|
||||
title="SSO login"
|
||||
body="Since you logged in via SSO, you cannot reset or modify your password."
|
||||
/>
|
||||
</Box>
|
||||
<Box width="500px" mt="5px">
|
||||
<Alert
|
||||
variant="warning"
|
||||
body="New password must contain at least 8 characters and must have at least one uppercase letter, one number and one symbol."
|
||||
/>
|
||||
</Box>
|
||||
<Box width="500px" mt="5px">
|
||||
<Alert
|
||||
variant="error"
|
||||
title="Error"
|
||||
body="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Divider sx={{ margin: `${theme.spacing(2)}` }} />
|
||||
</div>
|
||||
|
||||
@@ -41,13 +41,13 @@
|
||||
|
||||
#login-email-input {
|
||||
width: 308px;
|
||||
height: 11px;
|
||||
height: 1px;
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
|
||||
#login-password-input {
|
||||
width: 308px;
|
||||
height: 11px;
|
||||
height: 1px;
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
|
||||
|
||||
@@ -88,14 +88,32 @@ const theme = createTheme({
|
||||
},
|
||||
content: {
|
||||
pX: "80px",
|
||||
pY: "40px"
|
||||
pY: "40px",
|
||||
},
|
||||
gap: {
|
||||
small: "8px",
|
||||
medium: "12px",
|
||||
ml: "16px",
|
||||
large: "24px",
|
||||
xl: "40px"
|
||||
}
|
||||
xl: "40px",
|
||||
},
|
||||
alert: {
|
||||
info: {
|
||||
color: secondaryMain,
|
||||
bg: otherColorsWhite,
|
||||
border: "#d0d5dd",
|
||||
},
|
||||
error: {
|
||||
color: "#d92d20",
|
||||
bg: "hsla(0, 100%, 52%, 0.03)",
|
||||
border: "#f04438",
|
||||
},
|
||||
warning: {
|
||||
color: "#f79009",
|
||||
bg: "#fffaef",
|
||||
border: "#fecf60"
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default theme;
|
||||
@@ -7,6 +7,7 @@ const {
|
||||
recoveryValidation,
|
||||
recoveryTokenValidation,
|
||||
newPasswordValidation,
|
||||
deleteUserParamValidation
|
||||
} = require("../validation/joi");
|
||||
const logger = require("../utils/logger");
|
||||
require("dotenv").config();
|
||||
@@ -261,6 +262,86 @@ const resetPasswordController = async (req, res, next) => {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes a user and all associated monitors, checks, and alerts.
|
||||
*
|
||||
* @param {Object} req - The request object.
|
||||
* @param {Object} res - The response object.
|
||||
* @param {Function} next - The next middleware function.
|
||||
* @returns {Object} The response object with success status and message.
|
||||
* @throws {Error} If user validation fails or user is not found in the database.
|
||||
*/
|
||||
const deleteUserController = async (req, res, next) => {
|
||||
try {
|
||||
const token = req.headers.authorization.split(' ')[1];
|
||||
const decodedToken = jwt.decode(token)
|
||||
const { _id, email } = decodedToken;
|
||||
|
||||
const decodedTokenCastedAsRequest = {
|
||||
params: {
|
||||
userId: _id
|
||||
},
|
||||
body: {
|
||||
email
|
||||
}
|
||||
}
|
||||
|
||||
// Validate user
|
||||
await deleteUserParamValidation.validateAsync(decodedTokenCastedAsRequest.body);
|
||||
|
||||
// Check if the user exists
|
||||
const user = await req.db.getUserByEmail(decodedTokenCastedAsRequest);
|
||||
if (!user) {
|
||||
throw new Error(errorMessages.DB_USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 1. Find all the monitors associated with the user id
|
||||
const monitors = await req.db.getMonitorsByUserId(decodedTokenCastedAsRequest);
|
||||
|
||||
if (monitors) {
|
||||
// 2. Delete jobs associated with each monitor
|
||||
for (const monitor of monitors) {
|
||||
await req.jobQueue.deleteJob(monitor);
|
||||
}
|
||||
|
||||
// 3. Delete all checks associated with each monitor
|
||||
for (const monitor of monitors) {
|
||||
await req.db.deleteChecks(monitor._id);
|
||||
}
|
||||
|
||||
// 4. Delete all alerts associated with each monitor
|
||||
for (const monitor of monitors) {
|
||||
await req.db.deleteAlertByMonitorId(monitor._id);
|
||||
}
|
||||
|
||||
// 5. Delete each monitor
|
||||
await req.db.deleteMonitorsByUserId(user._id);
|
||||
|
||||
// 6. Delete the user by id
|
||||
await req.db.deleteUser(decodedTokenCastedAsRequest);
|
||||
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.AUTH_DELETE_USER,
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
msg: errorMessages.MONITOR_GET_BY_USER_ID,
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
registerController,
|
||||
loginController,
|
||||
@@ -269,4 +350,5 @@ module.exports = {
|
||||
recoveryRequestController,
|
||||
validateRecoveryTokenController,
|
||||
resetPasswordController,
|
||||
deleteUserController,
|
||||
};
|
||||
@@ -115,6 +115,28 @@ const updateUser = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Delete a user by ID
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<UserModel>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
const deleteUser = async (req, res) => {
|
||||
const userId = req.params.userId;
|
||||
try {
|
||||
const deletedUser = await UserModel.findByIdAndDelete(userId);
|
||||
if (!deletedUser) {
|
||||
throw new Error(errorMessages.DB_USER_NOT_FOUND);
|
||||
}
|
||||
return deletedUser;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Request a recovery token
|
||||
* @async
|
||||
@@ -319,6 +341,21 @@ const deleteAllMonitors = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete all monitors associated with a user ID
|
||||
* @async
|
||||
* @param {string} userId - The ID of the user whose monitors are to be deleted.
|
||||
* @returns {Promise} A promise that resolves when the operation is complete.
|
||||
*/
|
||||
const deleteMonitorsByUserId = async (userId) => {
|
||||
try {
|
||||
const result = await Monitor.deleteMany({ userId: userId });
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Edit a monitor by ID
|
||||
* @async
|
||||
@@ -520,6 +557,13 @@ const deleteAlert = async (alertId) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes alerts by monitor ID.
|
||||
*
|
||||
* @param {string} monitorId - The ID of the monitor.
|
||||
* @returns {Promise} A promise that resolves to the result of the delete operation.
|
||||
* @throws {Error} If an error occurs while deleting the alerts.
|
||||
*/
|
||||
const deleteAlertByMonitorId = async (monitorId) => {
|
||||
try {
|
||||
const result = await Alert.deleteMany({ monitorId });
|
||||
@@ -534,6 +578,7 @@ module.exports = {
|
||||
insertUser,
|
||||
getUserByEmail,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
requestRecoveryToken,
|
||||
validateRecoveryToken,
|
||||
resetPassword,
|
||||
@@ -555,4 +600,5 @@ module.exports = {
|
||||
editAlert,
|
||||
deleteAlert,
|
||||
deleteAlertByMonitorId,
|
||||
deleteMonitorsByUserId
|
||||
};
|
||||
Generated
+119
-370
File diff suppressed because it is too large.
Load diff
@@ -1,7 +1,9 @@
|
||||
const router = require("express").Router();
|
||||
const { verifyJWT } = require("../middleware/verifyJWT");
|
||||
const { verifyOwnership } = require("../middleware/verifyOwnership");
|
||||
const multer = require("multer");
|
||||
const upload = multer();
|
||||
const User = require("../models/user");
|
||||
|
||||
const {
|
||||
registerController,
|
||||
@@ -11,6 +13,7 @@ const {
|
||||
validateRecoveryTokenController,
|
||||
resetPasswordController,
|
||||
checkAdminController,
|
||||
deleteUserController
|
||||
} = require("../controllers/authController");
|
||||
|
||||
//Auth routes
|
||||
@@ -23,6 +26,7 @@ router.post(
|
||||
userEditController
|
||||
);
|
||||
router.get("/users/admin", checkAdminController);
|
||||
router.delete("/user/:userId", verifyJWT, verifyOwnership(User, "userId") , deleteUserController);
|
||||
|
||||
//Recovery routes
|
||||
router.post("/recovery/request", recoveryRequestController);
|
||||
|
||||
@@ -57,6 +57,7 @@ const successMessages = {
|
||||
AUTH_VERIFY_RECOVERY_TOKEN: "Recovery token verified successfully",
|
||||
AUTH_RESET_PASSWORD: "Password reset successfully",
|
||||
AUTH_ADMIN_CHECK: "Admin check completed successfully",
|
||||
AUTH_DELETE_USER: "User deleted successfully",
|
||||
|
||||
// Check Controller
|
||||
CHECK_CREATE: "Check created successfully",
|
||||
|
||||
@@ -46,6 +46,10 @@ const newPasswordValidation = joi.object({
|
||||
confirm: joi.string(),
|
||||
});
|
||||
|
||||
const deleteUserParamValidation = joi.object({
|
||||
email: joi.string().email().required(),
|
||||
});
|
||||
|
||||
//****************************************
|
||||
// Monitors
|
||||
//****************************************
|
||||
@@ -161,4 +165,5 @@ module.exports = {
|
||||
createCheckBodyValidation,
|
||||
getChecksParamValidation,
|
||||
deleteChecksParamValidation,
|
||||
deleteUserParamValidation
|
||||
};
|
||||
Reference in new issue
Block a user