diff --git a/client/src/Hooks/v1/userHooks.js b/client/src/Hooks/v1/userHooks.js
index 71b156594..f9c454450 100644
--- a/client/src/Hooks/v1/userHooks.js
+++ b/client/src/Hooks/v1/userHooks.js
@@ -7,7 +7,6 @@ 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);
@@ -54,8 +53,29 @@ export const useEditUser = (userId) => {
setIsLoading(false);
}
},
- [userId]
+ [userId, t]
);
- return [editUser, isLoading, error];
+ const changePassword = useCallback(
+ async (passwordForm) => {
+ try {
+ setIsLoading(true);
+
+ await networkService.changePasswordByAdmin({ userId, passwordForm });
+ createToast({
+ body: t("teamPanel.changeTeamPassword.success"),
+ });
+ } catch (error) {
+ createToast({
+ body: error.message,
+ });
+ setError(error);
+ } finally {
+ setIsLoading(false);
+ }
+ },
+ [userId, t]
+ );
+
+ return [editUser, isLoading, error, changePassword];
};
diff --git a/client/src/Pages/Account/components/ChangePasswordModal/index.jsx b/client/src/Pages/Account/components/ChangePasswordModal/index.jsx
new file mode 100644
index 000000000..cc284a675
--- /dev/null
+++ b/client/src/Pages/Account/components/ChangePasswordModal/index.jsx
@@ -0,0 +1,149 @@
+import { useState } from "react";
+import { Button, Stack } from "@mui/material";
+import { GenericDialog } from "@/Components/v1/Dialog/genericDialog";
+import TextInput from "@/Components/v1/Inputs/TextInput";
+import PasswordTooltip from "@/Pages/v1/Auth/components/PasswordTooltip";
+import { useTheme } from "@emotion/react";
+import { useTranslation } from "react-i18next";
+import { createToast } from "../../../../Utils/toastUtils";
+import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments";
+import usePasswordFeedback from "@/Pages/v1/Auth/hooks/usePasswordFeedback";
+import PropTypes from "prop-types";
+
+const ChangePasswordModal = ({ isSaving, isLoading, changePassword }) => {
+ const INITIAL_FORM_STATE = {
+ password: "",
+ confirm: "",
+ };
+ const theme = useTheme();
+ const { t } = useTranslation();
+ const { feedback, handlePasswordFeedback } = usePasswordFeedback();
+ const [form, setForm] = useState(INITIAL_FORM_STATE);
+ const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false);
+ const [errors, setErrors] = useState({});
+ const [isLoadingSubmit, setIsLoadingSubmit] = useState(false);
+ const closeChangePasswordModal = () => {
+ setIsChangePasswordOpen(false);
+ setForm(INITIAL_FORM_STATE);
+ };
+
+ const onChange = (e) => {
+ let { name, value } = e.target;
+ const updatedForm = { ...form, [name]: value };
+ setForm(updatedForm);
+
+ handlePasswordFeedback(updatedForm, name, value, form, errors, setErrors);
+ };
+ const isFormValid =
+ form.password.length > 1 &&
+ form.confirm.length > 1 &&
+ !errors.password &&
+ !errors.confirm;
+ const onsubmitChangePassword = async (event) => {
+ event.preventDefault();
+ if (!isFormValid) return;
+ const newPasswordForm = {
+ password: form.password,
+ };
+ try {
+ setIsLoadingSubmit(true);
+ await changePassword(newPasswordForm);
+ closeChangePasswordModal();
+ } catch (error) {
+ const errorMsg = error.response?.data?.msg || error.message || "unknownError";
+ createToast({
+ type: "error",
+ body: t(errorMsg),
+ });
+ } finally {
+ setIsLoadingSubmit(false);
+ }
+ };
+
+ return (
+ <>
+
+
+
+ }
+ sx={{ mb: theme.spacing(5) }}
+ />
+
+ }
+ sx={{ mb: theme.spacing(5) }}
+ />
+
+
+
+
+
+
+ >
+ );
+};
+
+ChangePasswordModal.propTypes = {
+ isSaving: PropTypes.bool.isRequired,
+ isLoading: PropTypes.bool.isRequired,
+ changePassword: PropTypes.func.isRequired,
+};
+
+export default ChangePasswordModal;
diff --git a/client/src/Pages/v1/Account/EditUser/index.jsx b/client/src/Pages/v1/Account/EditUser/index.jsx
index e2e976916..674a522b8 100644
--- a/client/src/Pages/v1/Account/EditUser/index.jsx
+++ b/client/src/Pages/v1/Account/EditUser/index.jsx
@@ -1,13 +1,12 @@
// Components
import Stack from "@mui/material/Stack";
-import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
import Search from "@/Components/v1/Inputs/Search/index.jsx";
import Button from "@mui/material/Button";
import RoleTable from "../components/RoleTable/index.jsx";
-
+import ChangePasswordModal from "@/Pages/Account/components/ChangePasswordModal/index.jsx";
// Utils
import { useParams } from "react-router-dom";
import { useTheme } from "@emotion/react";
@@ -15,9 +14,12 @@ import { useTranslation } from "react-i18next";
import { useGetUser, useEditUser } from "../../../../Hooks/v1/userHooks.js";
import { EDITABLE_ROLES, ROLES } from "../../../../Utils/roleUtils.js";
import { useEditUserForm, useValidateEditUserForm } from "./hooks/editUser.js";
+import { useSelector } from "react-redux";
const EditUser = () => {
+ const { user } = useSelector((state) => state.auth);
const { userId } = useParams();
+ const isSameUser = user?._id === userId;
const theme = useTheme();
const { t } = useTranslation();
const BREADCRUMBS = [
@@ -25,8 +27,8 @@ const EditUser = () => {
{ name: t("editUserPage.title"), path: "" },
];
- const [user, isLoading, error] = useGetUser(userId);
- const [editUser, isSaving, saveError] = useEditUser(userId);
+ const [userToEdit, isLoading, error] = useGetUser(userId);
+ const [editUser, isSaving, saveError, changePassword] = useEditUser(userId);
const [
form,
setForm,
@@ -34,7 +36,7 @@ const EditUser = () => {
handleDeleteRole,
searchInput,
handleSearchInput,
- ] = useEditUserForm(user);
+ ] = useEditUserForm(userToEdit);
const [errors, validateForm, validateField] = useValidateEditUserForm();
const onChange = (e) => {
@@ -104,7 +106,12 @@ const EditUser = () => {
roles={form?.role}
handleDeleteRole={handleDeleteRole}
/>
-
+
-
+ {!isSameUser && (
+
+ )}
+
);
diff --git a/client/src/Utils/NetworkService.js b/client/src/Utils/NetworkService.js
index dce0faaf5..6372be646 100644
--- a/client/src/Utils/NetworkService.js
+++ b/client/src/Utils/NetworkService.js
@@ -1136,6 +1136,15 @@ class NetworkService {
},
});
}
+
+ async changePasswordByAdmin(config) {
+ const { userId, passwordForm } = config;
+ return this.axiosInstance.put(`auth/users/${userId}/password`, passwordForm, {
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ }
}
export default NetworkService;
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index 2cd6ec6b5..da19c38da 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -425,7 +425,13 @@
}
}
},
- "role": "Role"
+ "role": "Role",
+ "changeTeamPassword": {
+ "changePasswordMenu": "Reset Password",
+ "title": "Reset team member password",
+ "description": "Create a new password for this team member. You will need to share the password with them securely.",
+ "success": "Password successfully reset. Make sure to provide the credentials to the member in a secure way."
+ }
},
"monitorState": {
"paused": "Paused",
diff --git a/server/src/controllers/v1/authController.js b/server/src/controllers/v1/authController.js
index 17ed517eb..e52de3849 100755
--- a/server/src/controllers/v1/authController.js
+++ b/server/src/controllers/v1/authController.js
@@ -10,10 +10,10 @@ import {
editUserByIdParamValidation,
editUserByIdBodyValidation,
editSuperadminUserByIdBodyValidation,
+ editUserPasswordByIdBodyValidation,
} from "../../validation/joi.js";
const SERVICE_NAME = "authController";
-
/**
* Authentication Controller
*
@@ -436,7 +436,7 @@ class AuthController extends BaseController {
async (req, res) => {
const roles = req?.user?.role;
if (!roles.includes("superadmin")) {
- throw createError("Unauthorized", 403);
+ throw this.errorService.createError("Unauthorized", 403);
}
const userId = req.params.userId;
@@ -456,6 +456,23 @@ class AuthController extends BaseController {
SERVICE_NAME,
"editUserById"
);
+ editUserPasswordById = this.asyncHandler(
+ async (req, res) => {
+ const roles = req?.user?.role;
+ if (!roles.includes("superadmin")) {
+ throw this.errorService.createError("Unauthorized", 403);
+ }
+
+ const userId = req.params.userId;
+ await editUserByIdParamValidation.validateAsync(req.params);
+ await editUserPasswordByIdBodyValidation.validateAsync(req.body);
+ const updatedPassword = req.body.password;
+ await this.userService.setPasswordByUserId(userId, updatedPassword);
+ return res.success({ msg: "Password reset successfully" });
+ },
+ SERVICE_NAME,
+ "editUserPasswordById"
+ );
}
export default AuthController;
diff --git a/server/src/routes/v1/authRoute.js b/server/src/routes/v1/authRoute.js
index d9b036b05..63b4c4201 100755
--- a/server/src/routes/v1/authRoute.js
+++ b/server/src/routes/v1/authRoute.js
@@ -25,6 +25,7 @@ class AuthRoutes {
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("/users/:userId/password", verifyJWT, isAllowed(["superadmin"]), this.authController.editUserPasswordById);
this.router.put("/user", verifyJWT, upload.single("profileImage"), this.authController.editUser);
this.router.delete("/user", verifyJWT, this.authController.deleteUser);
diff --git a/server/src/service/v1/business/userService.js b/server/src/service/v1/business/userService.js
index 0a965bced..7d7f7b014 100644
--- a/server/src/service/v1/business/userService.js
+++ b/server/src/service/v1/business/userService.js
@@ -211,5 +211,9 @@ class UserService {
editUserById = async (userId, user) => {
await this.db.userModule.editUserById(userId, user);
};
+ setPasswordByUserId = async (userId, password) => {
+ const updatedUser = await this.db.userModule.updateUser({ userId: userId, user: { password: password }, file: null });
+ return updatedUser;
+ };
}
export default UserService;
diff --git a/server/src/validation/joi.js b/server/src/validation/joi.js
index fa8dccde2..1cd5cd5d6 100755
--- a/server/src/validation/joi.js
+++ b/server/src/validation/joi.js
@@ -670,6 +670,10 @@ const editSuperadminUserByIdBodyValidation = joi.object({
.required(),
});
+const editUserPasswordByIdBodyValidation = joi.object({
+ password: joi.string().min(8).required().pattern(passwordPattern),
+});
+
export {
roleValidatior,
loginValidation,
@@ -739,4 +743,5 @@ export {
editUserByIdParamValidation,
editUserByIdBodyValidation,
editSuperadminUserByIdBodyValidation,
+ editUserPasswordByIdBodyValidation,
};