diff --git a/src/components/UserSettings.tsx b/src/components/UserSettings.tsx index 47549a7f..1906753a 100644 --- a/src/components/UserSettings.tsx +++ b/src/components/UserSettings.tsx @@ -51,6 +51,7 @@ import DisableEnableUsers from "./modals/DisableEnableUsers"; import DeleteUsers from "./modals/DeleteUsers"; import RebuildAutoMembership from "./modals/RebuildAutoMembership"; import UnlockUser from "./modals/UnlockUser"; +import ResetPassword from "./modals/ResetPassword"; export interface PropsToUserSettings { originalUser: Partial; @@ -160,11 +161,20 @@ const UserSettings = (props: PropsToUserSettings) => { return isLocked; }; + // 'Reset password' option + const [isResetPasswordModalOpen, setIsResetPasswordModalOpen] = + useState(false); + // Kebab const [isKebabOpen, setIsKebabOpen] = useState(false); const activeDropdownItems = [ - Reset password, + setIsResetPasswordModalOpen(true)} + > + Reset password + , { entriesToRebuild={userToRebuild} entity="users" /> + setIsResetPasswordModalOpen(false)} + /> ); }; diff --git a/src/components/modals/ResetPassword.tsx b/src/components/modals/ResetPassword.tsx new file mode 100644 index 00000000..0805c1c9 --- /dev/null +++ b/src/components/modals/ResetPassword.tsx @@ -0,0 +1,212 @@ +import React from "react"; +// PatternFly +import { + Button, + HelperText, + HelperTextItem, + ValidatedOptions, +} from "@patternfly/react-core"; +// Modals +import ModalWithFormLayout from "../layouts/ModalWithFormLayout"; +// Components +import PasswordInput from "../layouts/PasswordInput"; +// RPC +import { + ErrorResult, + PasswordChangePayload, + useChangePasswordMutation, +} from "src/services/rpc"; +// Hooks +import useAlerts from "src/hooks/useAlerts"; + +interface PropsToResetPassword { + uid: string | undefined; + isOpen: boolean; + onClose: () => void; +} + +const ResetPassword = (props: PropsToResetPassword) => { + // Alerts to show in the UI + const alerts = useAlerts(); + + // RPC hooks + const [resetPassword] = useChangePasswordMutation(); + + // Passwords + const [newPassword, setNewPassword] = React.useState(""); + const [verifyPassword, setVerifyPassword] = React.useState(""); + const [passwordHidden, setPasswordHidden] = React.useState(true); + const [verifyPasswordHidden, setVerifyPasswordHidden] = React.useState(true); + + // Verify password + const [passwordValidationResult, setPasswordValidationResult] = + React.useState({ + isError: false, + message: "", + pfError: ValidatedOptions.default, + }); + + const resetVerifyPassword = () => { + setPasswordValidationResult({ + isError: false, + message: "", + pfError: ValidatedOptions.default, + }); + }; + + // Fields + const fields = [ + { + id: "new-password", + name: "New Password", + pfComponent: ( + + ), + }, + { + id: "reset-password", + name: "Verify password", + pfComponent: ( + <> + + + + {passwordValidationResult.message} + + + + ), + }, + ]; + + // Checks that the passwords are the same + const validatePasswords = () => { + if (newPassword !== verifyPassword) { + const verifyPassVal = { + isError: true, + message: "Passwords must match", + pfError: ValidatedOptions.error, + }; + setPasswordValidationResult(verifyPassVal); + return true; // is error + } + resetVerifyPassword(); + return false; + }; + + // Verify the passwords are the same when we update a password value + React.useEffect(() => { + validatePasswords(); + }, [newPassword, verifyPassword]); + + // Reset fields and close modal + const resetFieldsAndCloseModal = () => { + // Reset fields + setNewPassword(""); + setVerifyPassword(""); + setPasswordHidden(true); + setVerifyPasswordHidden(true); + // Close modal + props.onClose(); + }; + + // on Reset Password + const onResetPassword = () => { + // API call to reset password + if (props.uid === undefined) { + // Alert error: no uid + alerts.addAlert( + "undefined-uid-error", + "No user selected to reset password", + "danger" + ); + } else { + const payload = { + uid: props.uid, + password: newPassword, + } as PasswordChangePayload; + + resetPassword(payload).then((response) => { + if ("data" in response) { + if (response.data.result) { + // Close modal + resetFieldsAndCloseModal(); + // Set alert: success + alerts.addAlert( + "reset-password-success", + "Changed password for user '" + props.uid + "'", + "success" + ); + } else if (response.data.error) { + // Set alert: error + const errorMessage = response.data.error as ErrorResult; + alerts.addAlert( + "reset-password-error", + errorMessage.message, + "danger" + ); + } + } + }); + } + }; + + const actions = [ + , + , + ]; + return ( + <> + + + + ); +}; + +export default ResetPassword; diff --git a/src/services/rpc.ts b/src/services/rpc.ts index 549cced7..3451ad9f 100644 --- a/src/services/rpc.ts +++ b/src/services/rpc.ts @@ -163,6 +163,12 @@ export const getBatchCommand = (commandData: Command[], apiVersion: string) => { return payloadBatchParams; }; +// Payload needed to change password +export interface PasswordChangePayload { + uid: string; + password: string; +} + // Endpoints that will be called from anywhere in the application. // Two types: // - Queries: https://redux-toolkit.js.org/rtk-query/usage/queries @@ -714,6 +720,22 @@ export const api = createApi({ }); }, }), + changePassword: build.mutation({ + query: (payload) => { + const params = [ + [payload.uid], + { + password: payload.password, + version: API_VERSION_BACKUP, + }, + ]; + + return getCommand({ + method: "passwd", + params: params, + }); + }, + }), }), }); @@ -782,4 +804,5 @@ export const { useEnableUserMutation, useDisableUserMutation, useUnlockUserMutation, + useChangePasswordMutation, } = api;