Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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<HTMLDivElement>();
Expand All @@ -47,6 +50,7 @@ const Header = () => {
const [displayMenuHelp, setMenuHelp] = useState(false);
const registrationModalRef = useRef<ModalHandle>(null);
const hotKeyCheatSheetModalRef = useRef<ModalHandle>(null);
const changePasswordModalRef = useRef<ModalHandle>(null);

const healthStatus = useAppSelector(state => getHealthStatus(state));
const errorCounter = useAppSelector(state => getErrorCount(state));
Expand Down Expand Up @@ -273,7 +277,11 @@ const Header = () => {
<LuChevronDown className="dropdown-icon" />
</BaseButton>
{/* Click on username, a dropdown menu with the option to logout opens */}
{displayMenuUser && <MenuUser />}
{displayMenuUser &&
<MenuUser
changePasswordRef={changePasswordModalRef}
/>
}
</div>
</nav>
</header>
Expand All @@ -286,6 +294,9 @@ const Header = () => {

{/* Hotkey Cheat Sheet */}
<HotKeyCheatSheet modalRef={hotKeyCheatSheetModalRef}/>

{/* Change Password Modal */}
<ChangePasswordModal modalRef={changePasswordModalRef}/>
</>
);
};
Expand Down Expand Up @@ -432,16 +443,35 @@ const MenuHelp = ({
);
};

const MenuUser = () => {
const MenuUser = ({
changePasswordRef,
}: {
changePasswordRef: React.RefObject<ModalHandle | null>
}) => {
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 (
<ul className="dropdown-ul">
<li>
<ButtonLikeAnchor onClick={() => showChangePasswordModal()}>
<span>{t("USER_MENU.CHANGE_PASSWORD")}</span>
</ButtonLikeAnchor>
</li>
<li>
<ButtonLikeAnchor onClick={() => logout()}>
<span className="logout-icon">{t("LOGOUT")}</span>
Expand Down
130 changes: 130 additions & 0 deletions src/components/shared/modals/ChangePassword.tsx
Original file line number Diff line number Diff line change
@@ -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<ModalHandle | null>
}) => {
const { t } = useTranslation();

const hideModal = () => {
modalRef.current?.close?.();
};


return (
<Modal
header={t("CHANGE_PASSWORD_MODAL.TITLE")}
classId="theme-details-modal"
ref={modalRef}
>
{/* component that manages tabs of theme details modal*/}
<ChangePassword
close={hideModal}
/>
</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 (
<Formik
initialValues={initialValues}
validationSchema={PasswordSchema}
onSubmit={values => handleSubmit(values)}
>
{formik => (
<>
<ModalContent>
<div className="form-container">
{!userDetails.manageable && (
<NotificationComponent
notification={{
type: "warning",
message: "NOTIFICATIONS.USER_NOT_MANAGEABLE",
id: 0,
}}
/>
)}
<div className="row">
<label>{t("USERS.USERS.DETAILS.FORM.PASSWORD")}</label>
<Field
type="password"
name="password"
disabled={!userDetails.manageable}
className={cn({
error: formik.touched.password && formik.errors.password,
disabled: !userDetails.manageable,
})}
placeholder={t("USERS.USERS.DETAILS.FORM.PASSWORD") + "..."}
/>
</div>
<div className="row">
<label>{t("USERS.USERS.DETAILS.FORM.REPEAT_PASSWORD")}</label>
<Field
type="password"
name="passwordConfirmation"
disabled={!userDetails.manageable}
className={cn({
error:
formik.touched.passwordConfirmation &&
formik.errors.passwordConfirmation,
disabled: !userDetails.manageable,
})}
placeholder={
t("USERS.USERS.DETAILS.FORM.REPEAT_PASSWORD") + "..."
}
/>
</div>
</div>
</ModalContent>
<WizardNavigationButtons
formik={formik}
previousPage={close}
createTranslationString="SUBMIT"
cancelTranslationString="CANCEL"
isLast
/>
</>
)}
</Formik>
);
};

export default ChangePasswordModal;
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
23 changes: 23 additions & 0 deletions src/slices/userDetailsSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions src/utils/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
Loading