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..454faee4dd --- /dev/null +++ b/src/components/shared/modals/ChangePassword.tsx @@ -0,0 +1,129 @@ +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 })); + 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..8d1d2b37e2 100644 --- a/src/slices/userDetailsSlice.ts +++ b/src/slices/userDetailsSlice.ts @@ -81,6 +81,28 @@ export const updateUserDetails = (params: { }); }; +export const updateUserPassword = (params: { + password: string, +}): AppThunk => dispatch => { + const { 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", 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"),