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, };