Merge pull request #2661 from bluewave-labs/feat/edit-users
feat: edit users, resolves #2659
This commit is contained in:
21 files changed
+619
-103
No files matched your search
@@ -77,6 +77,7 @@ const Search = ({
|
||||
labelVariant,
|
||||
labelSx = {},
|
||||
unit = "option",
|
||||
maxWidth = "100%",
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -16,6 +16,7 @@ const CustomTabList = ({ value, onChange, children, ...props }) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
marginBottom: theme.spacing(12),
|
||||
borderBottom: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
"& .MuiTabs-root": { height: "fit-content", minHeight: "0" },
|
||||
}}
|
||||
|
||||
@@ -8,4 +8,10 @@ const useIsAdmin = () => {
|
||||
return isAdmin;
|
||||
};
|
||||
|
||||
export { useIsAdmin };
|
||||
const useIsSuperAdmin = () => {
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
const isSuperAdmin = user?.role?.includes("superadmin");
|
||||
return isSuperAdmin;
|
||||
};
|
||||
|
||||
export { useIsAdmin, useIsSuperAdmin };
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { networkService } from "../main";
|
||||
import { createToast } from "../Utils/toastUtils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const useGetUser = (userId) => {
|
||||
const [user, setUser] = useState(undefined);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const fetchUser = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await networkService.getUserById({ userId });
|
||||
setUser(response?.data?.data);
|
||||
} catch (error) {
|
||||
createToast({
|
||||
body: error.message,
|
||||
});
|
||||
setError(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [userId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
fetchUser();
|
||||
}
|
||||
}, [userId, fetchUser]);
|
||||
|
||||
return [user, isLoading, error];
|
||||
};
|
||||
|
||||
export const useEditUser = (userId) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const editUser = useCallback(
|
||||
async (user) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await networkService.editUser({ userId, user });
|
||||
createToast({
|
||||
body: t("editUserPage.toast.successUserUpdate"),
|
||||
});
|
||||
} catch (error) {
|
||||
createToast({
|
||||
body: error.message,
|
||||
});
|
||||
setError(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[userId]
|
||||
);
|
||||
|
||||
return [editUser, isLoading, error];
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { ROLES } from "../../../../Utils/roleUtils";
|
||||
import { editUserValidation } from "../../../../Validation/validation";
|
||||
import Joi from "joi";
|
||||
import { createToast } from "../../../../Utils/toastUtils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
export const useEditUserForm = (user) => {
|
||||
const [searchInput, setSearchInput] = useState("");
|
||||
const [form, setForm] = useState({
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
role: [],
|
||||
});
|
||||
|
||||
// Effect to set user
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setForm({
|
||||
firstName: user?.firstName,
|
||||
lastName: user?.lastName,
|
||||
email: user?.email,
|
||||
role: user?.role,
|
||||
});
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const handleRoleChange = (value) => {
|
||||
const hasSuperAdmin = form.role.includes(ROLES.SUPERADMIN);
|
||||
const newRoles = value.map((item) => item.role);
|
||||
if (hasSuperAdmin && !newRoles.includes(ROLES.SUPERADMIN)) {
|
||||
newRoles.push(ROLES.SUPERADMIN);
|
||||
}
|
||||
|
||||
setForm({
|
||||
...form,
|
||||
role: newRoles,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteRole = (role) => {
|
||||
if (role === ROLES.SUPERADMIN) return;
|
||||
setForm({ ...form, role: form?.role?.filter((r) => r !== role) });
|
||||
};
|
||||
|
||||
const handleSearchInput = (value) => {
|
||||
setSearchInput(value);
|
||||
};
|
||||
|
||||
return [
|
||||
form,
|
||||
setForm,
|
||||
handleRoleChange,
|
||||
handleDeleteRole,
|
||||
searchInput,
|
||||
handleSearchInput,
|
||||
];
|
||||
};
|
||||
|
||||
export const useValidateEditUserForm = () => {
|
||||
const [errors, setErrors] = useState({});
|
||||
const { t } = useTranslation();
|
||||
const validateForm = (form) => {
|
||||
const { error } = editUserValidation.validate(form, {
|
||||
abortEarly: false,
|
||||
});
|
||||
const errs = error?.details;
|
||||
const errsObj = errs?.reduce((acc, curr) => {
|
||||
acc[curr.path[0]] = curr.message;
|
||||
return acc;
|
||||
}, {});
|
||||
setErrors(errsObj);
|
||||
|
||||
if (errs?.length > 0) {
|
||||
createToast({
|
||||
variant: "warning",
|
||||
hasIcon: true,
|
||||
title: t("editUserPage.toast.validationErrors"),
|
||||
body: errs.map((err) => t(err.message)),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateField = (name, value) => {
|
||||
const fieldSchema = Joi.object({
|
||||
[name]: editUserValidation.extract(name),
|
||||
});
|
||||
|
||||
const { error } = fieldSchema.validate({ [name]: value });
|
||||
|
||||
setErrors((prev) => {
|
||||
const prevErrors = { ...prev };
|
||||
if (error) prevErrors[name] = error.details[0].message;
|
||||
else delete prevErrors[name];
|
||||
return prevErrors;
|
||||
});
|
||||
};
|
||||
|
||||
return [errors, validateForm, validateField];
|
||||
};
|
||||
@@ -0,0 +1,122 @@
|
||||
// Components
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import Search from "../../../Components/Inputs/Search";
|
||||
import Button from "@mui/material/Button";
|
||||
import RoleTable from "../components/RoleTable";
|
||||
|
||||
// Utils
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useGetUser, useEditUser } from "../../../Hooks/userHooks";
|
||||
import { EDITABLE_ROLES, ROLES } from "../../../Utils/roleUtils";
|
||||
import { useEditUserForm, useValidateEditUserForm } from "./hooks/editUser";
|
||||
|
||||
const EditUser = () => {
|
||||
const { userId } = useParams();
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const BREADCRUMBS = [
|
||||
{ name: t("menu.team"), path: "/account/team" },
|
||||
{ name: t("editUserPage.title"), path: "" },
|
||||
];
|
||||
|
||||
const [user, isLoading, error] = useGetUser(userId);
|
||||
const [editUser, isSaving, saveError] = useEditUser(userId);
|
||||
const [
|
||||
form,
|
||||
setForm,
|
||||
handleRoleChange,
|
||||
handleDeleteRole,
|
||||
searchInput,
|
||||
handleSearchInput,
|
||||
] = useEditUserForm(user);
|
||||
const [errors, validateForm, validateField] = useValidateEditUserForm();
|
||||
|
||||
const onChange = (e) => {
|
||||
const name = e.target.name;
|
||||
const value = e.target.value;
|
||||
validateField(name, value);
|
||||
setForm({ ...form, [name]: value });
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const valid = validateForm(form);
|
||||
if (valid) {
|
||||
editUser(form);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<Typography variant="h2">{t("editUserPage.title")}</Typography>
|
||||
<Stack
|
||||
component="form"
|
||||
onSubmit={handleSubmit}
|
||||
gap={theme.spacing(12)}
|
||||
maxWidth="50%"
|
||||
>
|
||||
<TextInput
|
||||
name="firstName"
|
||||
label={t("editUserPage.form.firstName")}
|
||||
value={form?.firstName}
|
||||
onChange={onChange}
|
||||
error={errors?.firstName ? true : false}
|
||||
helperText={t(errors?.firstName)}
|
||||
/>
|
||||
<TextInput
|
||||
name="lastName"
|
||||
label={t("editUserPage.form.lastName")}
|
||||
value={form?.lastName}
|
||||
onChange={onChange}
|
||||
error={errors?.lastName ? true : false}
|
||||
helperText={t(errors?.lastName)}
|
||||
/>
|
||||
<TextInput
|
||||
name="email"
|
||||
label={t("editUserPage.form.email")}
|
||||
value={form?.email}
|
||||
disabled={true}
|
||||
error={errors?.email ? true : false}
|
||||
helperText={t(errors?.email)}
|
||||
/>
|
||||
<Search
|
||||
label={t("editUserPage.form.role")}
|
||||
filteredBy="role"
|
||||
inputValue={searchInput}
|
||||
handleInputChange={handleSearchInput}
|
||||
value={
|
||||
form?.role
|
||||
?.filter((role) => role !== ROLES.SUPERADMIN)
|
||||
.map((role) => ({ role, _id: role })) || []
|
||||
}
|
||||
options={EDITABLE_ROLES}
|
||||
multiple={true}
|
||||
handleChange={handleRoleChange}
|
||||
/>
|
||||
<RoleTable
|
||||
roles={form?.role}
|
||||
handleDeleteRole={handleDeleteRole}
|
||||
/>
|
||||
<Box>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="accent"
|
||||
loading={isLoading || isSaving}
|
||||
>
|
||||
{t("editUserPage.form.save")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditUser;
|
||||
+3
-3
@@ -2,10 +2,10 @@ import TabPanel from "@mui/lab/TabPanel";
|
||||
import { useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack, Typography, Button } from "@mui/material";
|
||||
import { PasswordEndAdornment } from "../../Inputs/TextInput/Adornments";
|
||||
import TextInput from "../../Inputs/TextInput";
|
||||
import { PasswordEndAdornment } from "../../../Components/Inputs/TextInput/Adornments";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import { newOrChangedCredentials } from "../../../Validation/validation";
|
||||
import Alert from "../../Alert";
|
||||
import Alert from "../../../Components/Alert";
|
||||
import { update } from "../../../Features/Auth/authSlice";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
+5
-6
@@ -2,16 +2,16 @@ import { useTheme } from "@emotion/react";
|
||||
import { useState } from "react";
|
||||
import TabPanel from "@mui/lab/TabPanel";
|
||||
import { Box, Button, Divider, Stack, Typography } from "@mui/material";
|
||||
import Avatar from "../../Avatar";
|
||||
import TextInput from "../../Inputs/TextInput";
|
||||
import ImageUpload from "../../Inputs/ImageUpload";
|
||||
import Avatar from "../../../Components/Avatar";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import ImageUpload from "../../../Components/Inputs/ImageUpload";
|
||||
import { newOrChangedCredentials } from "../../../Validation/validation";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { clearAuthState, deleteUser, update } from "../../../Features/Auth/authSlice";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import { GenericDialog } from "../../Dialog/genericDialog";
|
||||
import Dialog from "../../Dialog";
|
||||
import { GenericDialog } from "../../../Components/Dialog/genericDialog";
|
||||
import Dialog from "../../../Components/Dialog";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
/**
|
||||
@@ -191,7 +191,6 @@ const ProfilePanel = () => {
|
||||
>
|
||||
<Stack
|
||||
component="form"
|
||||
className="edit-profile-form"
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
gap={SPACING_GAP}
|
||||
@@ -0,0 +1,42 @@
|
||||
import Typography from "@mui/material/Typography";
|
||||
import DataTable from "../../../../Components/Table";
|
||||
import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ROLES } from "../../../../Utils/roleUtils";
|
||||
|
||||
const RoleTable = ({ roles, handleDeleteRole }) => {
|
||||
const { t } = useTranslation();
|
||||
const HEADERS = [
|
||||
{
|
||||
id: "name",
|
||||
content: <Typography>{t("editUserPage.table.roleHeader")}</Typography>,
|
||||
render: (row) => {
|
||||
return row;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "delete",
|
||||
content: <Typography>{t("editUserPage.table.actionHeader")}</Typography>,
|
||||
render: (row) => {
|
||||
if (row === ROLES.SUPERADMIN) return null;
|
||||
return (
|
||||
<DeleteOutlineRoundedIcon
|
||||
onClick={() => {
|
||||
handleDeleteRole(row);
|
||||
}}
|
||||
sx={{ cursor: "pointer" }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return (
|
||||
<DataTable
|
||||
headers={HEADERS}
|
||||
data={roles}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoleTable;
|
||||
+19
-7
@@ -3,14 +3,16 @@ import TabPanel from "@mui/lab/TabPanel";
|
||||
import { Button, ButtonGroup, Stack, Typography } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import TextInput from "../../Inputs/TextInput";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import { newOrChangedCredentials } from "../../../Validation/validation";
|
||||
import { networkService } from "../../../main";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import Select from "../../Inputs/Select";
|
||||
import { GenericDialog } from "../../Dialog/genericDialog";
|
||||
import DataTable from "../../Table/";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import { GenericDialog } from "../../../Components/Dialog/genericDialog";
|
||||
import DataTable from "../../../Components/Table";
|
||||
import { useGetInviteToken } from "../../../Hooks/inviteHooks";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useIsSuperAdmin } from "../../../Hooks/useIsAdmin";
|
||||
/**
|
||||
* TeamPanel component manages the organization and team members,
|
||||
* providing functionalities like renaming the organization, managing team members,
|
||||
@@ -34,8 +36,9 @@ const TeamPanel = () => {
|
||||
const [isDisabled, setIsDisabled] = useState(true);
|
||||
const [errors, setErrors] = useState({});
|
||||
const [isSendingInvite, setIsSendingInvite] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [getInviteToken, clearToken, isLoading, error, token] = useGetInviteToken();
|
||||
const isSuperAdmin = useIsSuperAdmin();
|
||||
|
||||
const headers = [
|
||||
{
|
||||
@@ -184,7 +187,6 @@ const TeamPanel = () => {
|
||||
|
||||
return (
|
||||
<TabPanel
|
||||
className="team-panel table-container"
|
||||
value="team"
|
||||
sx={{
|
||||
"& h1": {
|
||||
@@ -249,7 +251,17 @@ const TeamPanel = () => {
|
||||
<DataTable
|
||||
headers={headers}
|
||||
data={data}
|
||||
config={{ emptyView: t("teamPanel.noMembers") }}
|
||||
config={{
|
||||
emptyView: t("teamPanel.noMembers"),
|
||||
rowSX: {
|
||||
cursor: isSuperAdmin ? "pointer" : "default",
|
||||
},
|
||||
onRowClick: (row) => {
|
||||
if (isSuperAdmin) {
|
||||
navigate(`/account/team/${row.id}`);
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
.account h1.MuiTypography-root,
|
||||
.account p.MuiTypography-root,
|
||||
.account input,
|
||||
.account button,
|
||||
.account td,
|
||||
.account .MuiSelect-select {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.account h1.MuiTypography-root {
|
||||
font-weight: 600;
|
||||
}
|
||||
.account .MuiTabPanel-root {
|
||||
padding: 0;
|
||||
margin-top: 50px;
|
||||
}
|
||||
.account button:not(.MuiIconButton-root) {
|
||||
min-height: 34px;
|
||||
}
|
||||
.account .field {
|
||||
flex: 1;
|
||||
}
|
||||
#modal-delete-account,
|
||||
#modal-edit-org-name,
|
||||
#modal-invite-member {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
}
|
||||
|
||||
.account .MuiStack-root:has(span.MuiTypography-root.input-error) {
|
||||
position: relative;
|
||||
}
|
||||
.account:not(:has(#modal-invite-member)) span.MuiTypography-root.input-error {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
}
|
||||
|
||||
.account .MuiTableBody-root .MuiTableCell-root {
|
||||
padding: var(--env-var-spacing-1-plus) var(--env-var-spacing-2);
|
||||
}
|
||||
@@ -5,11 +5,10 @@ import { useSelector } from "react-redux";
|
||||
import { Box, Tab, useTheme } from "@mui/material";
|
||||
import CustomTabList from "../../Components/Tab";
|
||||
import TabContext from "@mui/lab/TabContext";
|
||||
import ProfilePanel from "../../Components/TabPanels/Account/ProfilePanel";
|
||||
import PasswordPanel from "../../Components/TabPanels/Account/PasswordPanel";
|
||||
import TeamPanel from "../../Components/TabPanels/Account/TeamPanel";
|
||||
import ProfilePanel from "./components/ProfilePanel";
|
||||
import PasswordPanel from "./components/PasswordPanel";
|
||||
import TeamPanel from "./components/TeamPanel";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
* Account component renders a settings page with tabs for Profile, Password, and Team settings.
|
||||
@@ -33,7 +32,9 @@ const Account = ({ open = "profile" }) => {
|
||||
{ name: t("menu.password"), value: "password" },
|
||||
{ name: t("menu.team"), value: "team" },
|
||||
];
|
||||
|
||||
const hideTeams = !requiredRoles.some((role) => user.role.includes(role));
|
||||
|
||||
if (hideTeams) {
|
||||
tabList = [
|
||||
{ name: t("menu.profile"), value: "profile" },
|
||||
@@ -63,7 +64,6 @@ const Account = ({ open = "profile" }) => {
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="account"
|
||||
px={theme.spacing(20)}
|
||||
py={theme.spacing(12)}
|
||||
>
|
||||
|
||||
@@ -42,6 +42,7 @@ import CreateNotifications from "../Pages/Notifications/create";
|
||||
|
||||
// Settings
|
||||
import Account from "../Pages/Account";
|
||||
import EditUser from "../Pages/Account/EditUser";
|
||||
import Settings from "../Pages/Settings";
|
||||
|
||||
import Maintenance from "../Pages/Maintenance";
|
||||
@@ -187,6 +188,14 @@ const Routes = () => {
|
||||
path="account/team"
|
||||
element={<Account open={"team"} />}
|
||||
/>
|
||||
<Route
|
||||
path="account/team/:userId"
|
||||
element={
|
||||
<RoleProtectedRoute roles={["superadmin"]}>
|
||||
<EditUser />
|
||||
</RoleProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="logs"
|
||||
|
||||
@@ -1099,6 +1099,24 @@ class NetworkService {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getUserById(config) {
|
||||
const userId = config?.userId;
|
||||
return this.axiosInstance.get(`auth/users/${userId}`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async editUser(config) {
|
||||
const { userId, user } = config;
|
||||
return this.axiosInstance.put(`auth/users/${userId}`, user, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default NetworkService;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export const ROLES = {
|
||||
SUPERADMIN: "superadmin",
|
||||
ADMIN: "admin",
|
||||
USER: "user",
|
||||
DEMO: "demo",
|
||||
};
|
||||
|
||||
export const VALID_ROLES = [ROLES.ADMIN, ROLES.USER, ROLES.DEMO];
|
||||
|
||||
export const EDITABLE_ROLES = [
|
||||
{ role: ROLES.ADMIN, _id: ROLES.ADMIN },
|
||||
{ role: ROLES.USER, _id: ROLES.USER },
|
||||
];
|
||||
@@ -1,5 +1,6 @@
|
||||
import joi from "joi";
|
||||
import dayjs from "dayjs";
|
||||
import { ROLES } from "../Utils/roleUtils";
|
||||
|
||||
const THRESHOLD_COMMON_BASE_MSG = "Threshold must be a number.";
|
||||
|
||||
@@ -429,6 +430,24 @@ const notificationValidation = joi.object({
|
||||
}),
|
||||
});
|
||||
|
||||
const editUserValidation = joi.object({
|
||||
firstName: nameSchema,
|
||||
lastName: lastnameSchema,
|
||||
role: joi
|
||||
.array()
|
||||
.items(joi.string().valid(...Object.values(ROLES)))
|
||||
.min(1)
|
||||
.messages({
|
||||
"array.min": "auth.common.fields.role.errors.min",
|
||||
}),
|
||||
email: joi
|
||||
.string()
|
||||
.required()
|
||||
.trim()
|
||||
.email({ tlds: { allow: false } })
|
||||
.lowercase(),
|
||||
});
|
||||
|
||||
export {
|
||||
newOrChangedCredentials,
|
||||
loginCredentials,
|
||||
@@ -441,4 +460,5 @@ export {
|
||||
statusPageValidation,
|
||||
logoImageValidation,
|
||||
notificationValidation,
|
||||
editUserValidation,
|
||||
};
|
||||
+73
-42
@@ -32,6 +32,11 @@
|
||||
"errors": {
|
||||
"incorrect": "The password you provided does not match our records"
|
||||
}
|
||||
},
|
||||
"role": {
|
||||
"errors": {
|
||||
"min": "At least one role is required"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": {
|
||||
@@ -212,8 +217,8 @@
|
||||
"checkFormError": "Please check the form for errors.",
|
||||
"checkFrequency": "Check frequency",
|
||||
"checkHooks": {
|
||||
"failureResolveOne": "Failed to resolve incident.",
|
||||
"failureResolveAll": "Failed to resolve all incidents."
|
||||
"failureResolveAll": "Failed to resolve all incidents.",
|
||||
"failureResolveOne": "Failed to resolve incident."
|
||||
},
|
||||
"checkingEvery": "Checking every",
|
||||
"city": "CITY",
|
||||
@@ -229,9 +234,6 @@
|
||||
"unknownError": "Unknown error"
|
||||
}
|
||||
},
|
||||
"general": {
|
||||
"noOptionsFound": "No {{unit}} found"
|
||||
},
|
||||
"commonSave": "Save",
|
||||
"commonSaving": "Saving...",
|
||||
"companyName": "Company name",
|
||||
@@ -251,6 +253,8 @@
|
||||
"createMonitor": "Create monitor",
|
||||
"createNew": "Create new",
|
||||
"createNotifications": {
|
||||
"dialogDeleteConfirm": "Delete",
|
||||
"dialogDeleteTitle": "Are you sure you want to delete this notification?",
|
||||
"discordSettings": {
|
||||
"description": "Configure your Discord webhook here",
|
||||
"title": "Discord",
|
||||
@@ -281,6 +285,7 @@
|
||||
"webhookLabel": "Slack webhook URL",
|
||||
"webhookPlaceholder": "https://hooks.slack.com/services/..."
|
||||
},
|
||||
"testNotification": "Test notification",
|
||||
"title": "Create notification channel",
|
||||
"typeSettings": {
|
||||
"description": "Select the type of notification channel you want to create.",
|
||||
@@ -292,10 +297,7 @@
|
||||
"title": "Webhook",
|
||||
"webhookLabel": "Webhook URL",
|
||||
"webhookPlaceholder": "https://your-server.com/webhook"
|
||||
},
|
||||
"testNotification": "Test notification",
|
||||
"dialogDeleteTitle": "Are you sure you want to delete this notification?",
|
||||
"dialogDeleteConfirm": "Delete"
|
||||
}
|
||||
},
|
||||
"createYour": "Create your",
|
||||
"date&Time": "Date & Time",
|
||||
@@ -305,6 +307,27 @@
|
||||
"deleteStatusPage": "Do you want to delete this status page?",
|
||||
"deleteStatusPageConfirm": "Yes, delete status page",
|
||||
"deleteStatusPageDescription": "Once deleted, your status page cannot be retrieved.",
|
||||
"diagnosticsPage": {
|
||||
"diagnosticDescription": "System diagnostics",
|
||||
"gauges": {
|
||||
"heapAllocationSubtitle": "% of available memory",
|
||||
"heapAllocationTitle": "Heap allocation",
|
||||
"heapUsageSubtitle": "% of available memory",
|
||||
"heapUsageTitle": "Heap usage",
|
||||
"heapUtilizationSubtitle": "% of allocated",
|
||||
"heapUtilizationTitle": "Heap utilization",
|
||||
"instantCpuUsageSubtitle": "% of 1s used by CPU",
|
||||
"instantCpuUsageTitle": "Instant CPU usage"
|
||||
},
|
||||
"stats": {
|
||||
"eventLoopDelayTitle": "Event loop delay",
|
||||
"osMemoryLimitTitle": "OS Memory Limit",
|
||||
"totalHeapSizeTitle": "Total heap size",
|
||||
"uptimeTitle": "Uptime",
|
||||
"usedHeapSizeTitle": "Used heap size"
|
||||
},
|
||||
"statsDescription": "System statistics"
|
||||
},
|
||||
"disk": "Disk",
|
||||
"diskUsage": "Disk Usage",
|
||||
"displayName": "Display name",
|
||||
@@ -355,6 +378,24 @@
|
||||
"duration": "Duration",
|
||||
"edit": "Edit",
|
||||
"editMaintenance": "Edit maintenance",
|
||||
"editUserPage": {
|
||||
"form": {
|
||||
"email": "Email",
|
||||
"firstName": "First name",
|
||||
"lastName": "Last name",
|
||||
"role": "Roles",
|
||||
"save": "Save"
|
||||
},
|
||||
"table": {
|
||||
"actionHeader": "Action",
|
||||
"roleHeader": "Role"
|
||||
},
|
||||
"title": "Edit user",
|
||||
"toast": {
|
||||
"successUserUpdate": "User updated successfully",
|
||||
"validationErrors": "Validation errors"
|
||||
}
|
||||
},
|
||||
"editing": "Editing...",
|
||||
"emailSent": "Email sent successfully",
|
||||
"errorInvalidFieldId": "Invalid field ID provided",
|
||||
@@ -385,6 +426,9 @@
|
||||
"friendlyNameInput": "Friendly name",
|
||||
"friendlyNamePlaceholder": "Maintenance at __ : __ for ___ minutes",
|
||||
"gb": "GB",
|
||||
"general": {
|
||||
"noOptionsFound": "No {{unit}} found"
|
||||
},
|
||||
"greeting": {
|
||||
"append": "The afternoon is your playground—let's make it epic!",
|
||||
"overview": "Here's an overview of your {{type}} monitors.",
|
||||
@@ -399,24 +443,24 @@
|
||||
"incidentsOptionsHeader": "Incidents for:",
|
||||
"incidentsOptionsHeaderFilterAll": "All",
|
||||
"incidentsOptionsHeaderFilterBy": "Filter by:",
|
||||
"incidentsOptionsHeaderTotalIncidents": "Total Incidents",
|
||||
"incidentsOptionsHeaderFilterCannotResolve": "Cannot Resolve",
|
||||
"incidentsTableResolvedAt": "Resolved at",
|
||||
"incidentsOptionsHeaderFilterDown": "Down",
|
||||
"incidentsTableActionResolve": "Resolve",
|
||||
"incidentsPageActionResolve": "Resolve all incidents",
|
||||
"incidentsOptionsHeaderFilterResolved": "Resolved",
|
||||
"incidentsOptionsHeaderLastDay": "Last day",
|
||||
"incidentsOptionsHeaderLastHour": "Last hour",
|
||||
"incidentsOptionsHeaderLastWeek": "Last week",
|
||||
"incidentsOptionsHeaderShow": "Show:",
|
||||
"incidentsOptionsHeaderTotalIncidents": "Total Incidents",
|
||||
"incidentsOptionsPlaceholderAllServers": "All servers",
|
||||
"incidentsPageActionResolve": "Resolve all incidents",
|
||||
"incidentsPageTitle": "Incidents",
|
||||
"incidentsTableActionResolve": "Resolve",
|
||||
"incidentsTableDateTime": "Date & Time",
|
||||
"incidentsTableMessage": "Message",
|
||||
"incidentsTableMonitorName": "Monitor Name",
|
||||
"incidentsTableNoIncidents": "No incidents recorded",
|
||||
"incidentsTablePaginationLabel": "incidents",
|
||||
"incidentsTableResolvedAt": "Resolved at",
|
||||
"incidentsTableStatus": "Status",
|
||||
"incidentsTableStatusCode": "Status Code",
|
||||
"infrastructureAlertNotificationDescription": "Send a notification to user(s) when thresholds exceed a specified percentage.",
|
||||
@@ -469,36 +513,15 @@
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"diagnostics": "Diagnostics",
|
||||
"logs": "Server logs",
|
||||
"queue": "Job queue",
|
||||
"diagnostics": "Diagnostics"
|
||||
"queue": "Job queue"
|
||||
},
|
||||
"title": "Logs",
|
||||
"toast": {
|
||||
"fetchLogsSuccess": "Logs fetched successfully"
|
||||
}
|
||||
},
|
||||
"diagnosticsPage": {
|
||||
"diagnosticDescription": "System diagnostics",
|
||||
"statsDescription": "System statistics",
|
||||
"gauges": {
|
||||
"heapAllocationTitle": "Heap allocation",
|
||||
"heapAllocationSubtitle": "% of available memory",
|
||||
"heapUsageTitle": "Heap usage",
|
||||
"heapUsageSubtitle": "% of available memory",
|
||||
"heapUtilizationTitle": "Heap utilization",
|
||||
"heapUtilizationSubtitle": "% of allocated",
|
||||
"instantCpuUsageTitle": "Instant CPU usage",
|
||||
"instantCpuUsageSubtitle": "% of 1s used by CPU"
|
||||
},
|
||||
"stats": {
|
||||
"eventLoopDelayTitle": "Event loop delay",
|
||||
"uptimeTitle": "Uptime",
|
||||
"usedHeapSizeTitle": "Used heap size",
|
||||
"totalHeapSizeTitle": "Total heap size",
|
||||
"osMemoryLimitTitle": "OS Memory Limit"
|
||||
}
|
||||
},
|
||||
"low": "low",
|
||||
"maintenance": "maintenance",
|
||||
"maintenanceRepeat": "Maintenance Repeat",
|
||||
@@ -545,8 +568,8 @@
|
||||
"message": "Message",
|
||||
"monitor": "monitor",
|
||||
"monitorActions": {
|
||||
"deleteSuccess": "Monitor deleted successfully",
|
||||
"deleteFailed": "Failed to delete monitor",
|
||||
"deleteSuccess": "Monitor deleted successfully",
|
||||
"details": "Details",
|
||||
"export": "Export Monitors",
|
||||
"import": "Import Monitors",
|
||||
@@ -856,6 +879,14 @@
|
||||
"pending": "Pending...",
|
||||
"up": "Your site is up."
|
||||
},
|
||||
"statusPage": {
|
||||
"contents": "Contents",
|
||||
"createSuccess": "Status page created successfully",
|
||||
"deleteFailed": "Failed to delete status page",
|
||||
"deleteSuccess": "Status page deleted successfully",
|
||||
"generalSettings": "General settings",
|
||||
"updateSuccess": "Status page updated successfully"
|
||||
},
|
||||
"statusPageCreate": {
|
||||
"buttonSave": "Save"
|
||||
},
|
||||
@@ -919,13 +950,13 @@
|
||||
"testLocale": "testLocale",
|
||||
"testNotificationsDisabled": "There are no notifications setup for this monitor. You need to add one by clicking 'Configure' button",
|
||||
"time": {
|
||||
"threeMinutes": "3 minutes",
|
||||
"fiveMinutes": "5 minutes",
|
||||
"tenMinutes": "10 minutes",
|
||||
"twentyMinutes": "20 minutes",
|
||||
"oneHour": "1 hour",
|
||||
"oneDay": "1 day",
|
||||
"oneWeek": "1 week"
|
||||
"oneHour": "1 hour",
|
||||
"oneWeek": "1 week",
|
||||
"tenMinutes": "10 minutes",
|
||||
"threeMinutes": "3 minutes",
|
||||
"twentyMinutes": "20 minutes"
|
||||
},
|
||||
"timeZoneInfo": "All dates and times are in GMT+0 time zone.",
|
||||
"timezone": "Timezone",
|
||||
|
||||
@@ -5,6 +5,10 @@ import {
|
||||
recoveryValidation,
|
||||
recoveryTokenValidation,
|
||||
newPasswordValidation,
|
||||
getUserByIdParamValidation,
|
||||
editUserByIdParamValidation,
|
||||
editUserByIdBodyValidation,
|
||||
editSuperadminUserByIdBodyValidation,
|
||||
} from "../validation/joi.js";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { getTokenFromHeaders } from "../utils/utils.js";
|
||||
@@ -393,6 +397,53 @@ class AuthController {
|
||||
SERVICE_NAME,
|
||||
"getAllUsers"
|
||||
);
|
||||
|
||||
getUserById = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await getUserByIdParamValidation.validateAsync(req.params);
|
||||
const userId = req?.params?.userId;
|
||||
const roles = req?.user?.role;
|
||||
|
||||
if (!userId) {
|
||||
throw new Error("No user ID in request");
|
||||
}
|
||||
|
||||
if (!roles || roles.length === 0) {
|
||||
throw new Error("No roles in request");
|
||||
}
|
||||
|
||||
const user = await this.db.getUserById(roles, userId);
|
||||
|
||||
return res.success({ msg: "ok", data: user });
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getUserById"
|
||||
);
|
||||
|
||||
editUserById = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const roles = req?.user?.role;
|
||||
if (!roles.includes("superadmin")) {
|
||||
throw createError("Unauthorized", 403);
|
||||
}
|
||||
|
||||
const userId = req.params.userId;
|
||||
const user = { ...req.body };
|
||||
|
||||
await editUserByIdParamValidation.validateAsync(req.params);
|
||||
// If this is superadmin self edit, allow "superadmin" role
|
||||
if (userId === req.user._id) {
|
||||
await editSuperadminUserByIdBodyValidation.validateAsync(req.body);
|
||||
} else {
|
||||
await editUserByIdBodyValidation.validateAsync(req.body);
|
||||
}
|
||||
|
||||
await this.db.editUserById(userId, user);
|
||||
return res.success({ msg: "ok" });
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"editUserById"
|
||||
);
|
||||
}
|
||||
|
||||
export default AuthController;
|
||||
@@ -209,4 +209,33 @@ const logoutUser = async (userId) => {
|
||||
}
|
||||
};
|
||||
|
||||
export { insertUser, getUserByEmail, updateUser, deleteUser, deleteTeam, deleteAllOtherUsers, getAllUsers, logoutUser };
|
||||
const getUserById = async (roles, userId) => {
|
||||
try {
|
||||
if (!roles.includes("superadmin")) {
|
||||
throw new Error("User is not a superadmin");
|
||||
}
|
||||
|
||||
const user = await UserModel.findById(userId).select("-password").select("-profileImage");
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getUserById";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const editUserById = async (userId, user) => {
|
||||
try {
|
||||
await UserModel.findByIdAndUpdate(userId, user, { new: true }).select("-password").select("-profileImage");
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "editUserById";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export { insertUser, getUserByEmail, updateUser, deleteUser, deleteTeam, deleteAllOtherUsers, getAllUsers, logoutUser, getUserById, editUserById };
|
||||
@@ -25,6 +25,8 @@ class AuthRoutes {
|
||||
this.router.get("/users/superadmin", this.authController.checkSuperadminExists);
|
||||
|
||||
this.router.get("/users", verifyJWT, isAllowed(["admin", "superadmin"]), this.authController.getAllUsers);
|
||||
this.router.get("/users/:userId", verifyJWT, isAllowed(["superadmin"]), this.authController.getUserById);
|
||||
this.router.put("/users/:userId", verifyJWT, isAllowed(["superadmin"]), this.authController.editUserById);
|
||||
|
||||
this.router.put("/user", verifyJWT, upload.single("profileImage"), this.authController.editUser);
|
||||
this.router.delete("/user", verifyJWT, this.authController.deleteUser);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import joi from "joi";
|
||||
import { ROLES, VALID_ROLES } from "../../client/src/Utils/roleUtils.js";
|
||||
|
||||
//****************************************
|
||||
// Custom Validators
|
||||
@@ -652,6 +653,36 @@ const sendTestEmailBodyValidation = joi.object({
|
||||
systemEmailTLSServername: joi.string().allow("").optional(),
|
||||
});
|
||||
|
||||
const getUserByIdParamValidation = joi.object({
|
||||
userId: joi.string().required(),
|
||||
});
|
||||
|
||||
const editUserByIdParamValidation = joi.object({
|
||||
userId: joi.string().required(),
|
||||
});
|
||||
|
||||
const editUserByIdBodyValidation = joi.object({
|
||||
firstName: nameValidation.required(),
|
||||
lastName: nameValidation.required(),
|
||||
email: joi.string().email().required(),
|
||||
role: joi
|
||||
.array()
|
||||
.items(joi.string().valid(...VALID_ROLES))
|
||||
.min(1)
|
||||
.required(),
|
||||
});
|
||||
|
||||
const editSuperadminUserByIdBodyValidation = joi.object({
|
||||
firstName: nameValidation.required(),
|
||||
lastName: nameValidation.required(),
|
||||
email: joi.string().email().required(),
|
||||
role: joi
|
||||
.array()
|
||||
.items(joi.string().valid(...VALID_ROLES, ROLES.SUPERADMIN))
|
||||
.min(1)
|
||||
.required(),
|
||||
});
|
||||
|
||||
export {
|
||||
roleValidatior,
|
||||
loginValidation,
|
||||
@@ -719,4 +750,8 @@ export {
|
||||
webhookConfigValidation,
|
||||
createAnnouncementValidation,
|
||||
sendTestEmailBodyValidation,
|
||||
getUserByIdParamValidation,
|
||||
editUserByIdParamValidation,
|
||||
editUserByIdBodyValidation,
|
||||
editSuperadminUserByIdBodyValidation,
|
||||
};
|
||||
Reference in new issue
Block a user