From 75f2f2f620edb55e2ed967087e0cc620232c6036 Mon Sep 17 00:00:00 2001 From: Arnei Date: Mon, 11 May 2026 13:47:29 +0200 Subject: [PATCH] Add change password option Adds a new button in the user menu (the dropdown in the top right corner) that allows users to change their own password. The idea is to allow users to change their password without having to give them full access to the users table. --- src/components/Header.tsx | 34 ++++- .../shared/modals/ChangePassword.tsx | 130 ++++++++++++++++++ .../adminui/languages/lang-en_US.json | 6 + src/slices/userDetailsSlice.ts | 23 ++++ src/utils/validate.ts | 9 ++ 5 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 src/components/shared/modals/ChangePassword.tsx diff --git a/src/components/Header.tsx b/src/components/Header.tsx index abc054a621..10562100d4 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -8,6 +8,7 @@ import { setSpecificServiceFilter } from "../slices/tableFilterSlice"; import { getErrorCount, getHealthStatus } from "../selectors/healthSelectors"; import { getOrgProperties, + getUserBasicInfo, getUserInformation, } from "../selectors/userInfoSelectors"; import { availableHotkeys } from "../configs/hotkeysConfig"; @@ -27,6 +28,8 @@ import { ModalHandle } from "./shared/modals/Modal"; import { broadcastLogout } from "../utils/broadcastSync"; import BaseButton from "./shared/BaseButton"; import { LuBell, LuCheck, LuChevronDown, LuCirclePlay, LuMessageCircleQuestion, LuVideo } from "react-icons/lu"; +import { fetchUserDetails } from "../slices/userDetailsSlice"; +import ChangePasswordModal from "./shared/modals/ChangePassword"; // References for detecting a click outside of the container of the dropdown menus const containerLang = React.createRef(); @@ -47,6 +50,7 @@ const Header = () => { const [displayMenuHelp, setMenuHelp] = useState(false); const registrationModalRef = useRef(null); const hotKeyCheatSheetModalRef = useRef(null); + const changePasswordModalRef = useRef(null); const healthStatus = useAppSelector(state => getHealthStatus(state)); const errorCounter = useAppSelector(state => getErrorCount(state)); @@ -273,7 +277,11 @@ const Header = () => { {/* Click on username, a dropdown menu with the option to logout opens */} - {displayMenuUser && } + {displayMenuUser && + + } @@ -286,6 +294,9 @@ const Header = () => { {/* Hotkey Cheat Sheet */} + + {/* Change Password Modal */} + ); }; @@ -432,16 +443,35 @@ const MenuHelp = ({ ); }; -const MenuUser = () => { +const MenuUser = ({ + changePasswordRef, +}: { + changePasswordRef: React.RefObject +}) => { const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const user = useAppSelector(getUserBasicInfo); const logout = () => { // Here we broadcast logout, in order to redirect other tabs to login page! broadcastLogout(); window.location.href = "/j_spring_security_logout"; }; + + const showChangePasswordModal = async () => { + await dispatch(fetchUserDetails(user.username)); + + changePasswordRef.current?.open(); + }; + return (
    +
  • + showChangePasswordModal()}> + {t("USER_MENU.CHANGE_PASSWORD")} + +
  • logout()}> {t("LOGOUT")} diff --git a/src/components/shared/modals/ChangePassword.tsx b/src/components/shared/modals/ChangePassword.tsx new file mode 100644 index 0000000000..daaa6f7764 --- /dev/null +++ b/src/components/shared/modals/ChangePassword.tsx @@ -0,0 +1,130 @@ +import { Formik } from "formik"; +import { useAppDispatch, useAppSelector } from "../../../store"; +import { getUserDetails } from "../../../selectors/userDetailsSelectors"; +import ModalContent from "./ModalContent"; +import { updateUserPassword } from "../../../slices/userDetailsSlice"; +import { NotificationComponent } from "../Notifications"; +import { Field } from "../Field"; +import cn from "classnames"; +import { useTranslation } from "react-i18next"; +import { PasswordSchema } from "../../../utils/validate"; +import { Modal, ModalHandle } from "./Modal"; +import WizardNavigationButtons from "../wizard/WizardNavigationButtons"; + +/** + * This component allows the current user to change their password. + */ +const ChangePasswordModal = ({ + modalRef, +}: { + modalRef: React.RefObject +}) => { + const { t } = useTranslation(); + + const hideModal = () => { + modalRef.current?.close?.(); + }; + + + return ( + + {/* component that manages tabs of theme details modal*/} + + + ); +}; + +const ChangePassword = ({ + close, +}: { + close: () => void, +}) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const userDetails = useAppSelector(state => getUserDetails(state)); + + const initialValues = { + password: "", + passwordConfirmation: "", + }; + + const handleSubmit = (values: { + password: string, + }) => { + dispatch(updateUserPassword({ password: values.password, username: userDetails.username })); + close(); + }; + + + return ( + handleSubmit(values)} + > + {formik => ( + <> + +
    + {!userDetails.manageable && ( + + )} +
    + + +
    +
    + + +
    +
    +
    + + + )} +
    + ); +}; + +export default ChangePasswordModal; diff --git a/src/i18n/org/opencastproject/adminui/languages/lang-en_US.json b/src/i18n/org/opencastproject/adminui/languages/lang-en_US.json index 8c0fc418da..2f3555b5df 100644 --- a/src/i18n/org/opencastproject/adminui/languages/lang-en_US.json +++ b/src/i18n/org/opencastproject/adminui/languages/lang-en_US.json @@ -2186,5 +2186,11 @@ "TITLE": "Terms of use", "NOCONTENT": "Content not available", "AGREE": "I have read and agree to the terms of use" + }, + "USER_MENU": { + "CHANGE_PASSWORD": "Change password" + }, + "CHANGE_PASSWORD_MODAL": { + "TITLE": "Change your password" } } diff --git a/src/slices/userDetailsSlice.ts b/src/slices/userDetailsSlice.ts index 17bf2f81fc..02316aba0a 100644 --- a/src/slices/userDetailsSlice.ts +++ b/src/slices/userDetailsSlice.ts @@ -81,6 +81,29 @@ export const updateUserDetails = (params: { }); }; +export const updateUserPassword = (params: { + password: string, + username: UserDetailsState["name"] +}): AppThunk => dispatch => { + const { username, password } = params; + + // get URL params used for put request + const data = new URLSearchParams(); + data.append("password", password); + + // PUT request + axios + .put(`/admin-ng/users/self/${username}.json`, data) + .then(response => { + console.info(response); + dispatch(addNotification({ type: "success", key: "USER_UPDATED" })); + }) + .catch(response => { + console.error(response); + dispatch(addNotification({ type: "error", key: "USER_NOT_SAVED" })); + }); +}; + const userDetailsSlice = createSlice({ name: "userDetails", initialState, diff --git a/src/utils/validate.ts b/src/utils/validate.ts index 9ef13471b9..d7b7e7662d 100644 --- a/src/utils/validate.ts +++ b/src/utils/validate.ts @@ -243,6 +243,15 @@ export const EditUserSchema = Yup.object().shape({ }), }); +export const PasswordSchema = Yup.object().shape({ + passwordConfirmation: Yup.string().when("password", { + is: (value: any) => !!value, + then: () => Yup.string() + .oneOf([Yup.ref("password"), undefined], "Passwords must match") + .required("Required"), + }), +}); + // Validation Schema used in group details modal export const EditGroupSchema = Yup.object().shape({ name: Yup.string().required("Required"),