Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Active users][Settings][Kebab] Add 'Reset password' option #215

Merged
merged 1 commit into from
Jan 11, 2024
Merged
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
17 changes: 16 additions & 1 deletion src/components/UserSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<User>;
Expand Down Expand Up @@ -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 = [
<DropdownItem key="reset password">Reset password</DropdownItem>,
<DropdownItem
key="reset password"
onClick={() => setIsResetPasswordModalOpen(true)}
>
Reset password
</DropdownItem>,
<DropdownItem
key="enable"
isDisabled={!props.user.nsaccountlock}
Expand Down Expand Up @@ -488,6 +498,11 @@ const UserSettings = (props: PropsToUserSettings) => {
entriesToRebuild={userToRebuild}
entity="users"
/>
<ResetPassword
uid={props.user.uid}
isOpen={isResetPasswordModalOpen}
onClose={() => setIsResetPasswordModalOpen(false)}
/>
</>
);
};
Expand Down
212 changes: 212 additions & 0 deletions src/components/modals/ResetPassword.tsx
Original file line number Diff line number Diff line change
@@ -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: (
<PasswordInput
id="reset-password-new-password"
name="password"
value={newPassword}
aria-label="new password text input"
onFocus={resetVerifyPassword}
onChange={setNewPassword}
onRevealHandler={setPasswordHidden}
passwordHidden={passwordHidden}
/>
),
},
{
id: "reset-password",
name: "Verify password",
pfComponent: (
<>
<PasswordInput
id="reset-password-verify-password"
name="password2"
value={verifyPassword}
aria-label="verify password text input"
onFocus={resetVerifyPassword}
onChange={setVerifyPassword}
onRevealHandler={setVerifyPasswordHidden}
passwordHidden={verifyPasswordHidden}
validated={passwordValidationResult.pfError}
/>
<HelperText>
<HelperTextItem variant="error">
{passwordValidationResult.message}
</HelperTextItem>
</HelperText>
</>
),
},
];

// 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 = [
<Button
key={"reset-password"}
variant="primary"
onClick={onResetPassword}
isDisabled={
passwordValidationResult.isError ||
newPassword === "" ||
verifyPassword === ""
}
>
Reset password
</Button>,
<Button
key={"cancel-reset-password"}
variant="link"
onClick={resetFieldsAndCloseModal}
>
Cancel
</Button>,
];
return (
<>
<alerts.ManagedAlerts />
<ModalWithFormLayout
variantType="small"
modalPosition="top"
title="Reset password"
description="Reset the password for the selected user"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be improved later. But from UX perspective I'd rather see: Reset the password for user $UID. The reason is that in the "settings" page the user is not selected and it might be also a confirmation to the admin what user it applies to.

Alternatively, I'd remove the "description" altogether as it, in current form, doesn't add more info than what is already showed in the title.

CC @bdellasc

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with your point, I think is more useful to refer to the current user (especially when this is an operation that is not made in bulk).

I can either change the title to the one that you are proposing (Reset the password for user $UID) or leave it as it is from now until we can discuss this with @bdellasc . I personally prefer the latter because it can bring an interesting UX perspective :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both works for me. So we can go with your preferred.

formId="reset-password-form"
fields={fields}
show={props.isOpen}
onClose={resetFieldsAndCloseModal}
actions={actions}
/>
</>
);
};

export default ResetPassword;
23 changes: 23 additions & 0 deletions src/services/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -714,6 +720,22 @@ export const api = createApi({
});
},
}),
changePassword: build.mutation<FindRPCResponse, PasswordChangePayload>({
query: (payload) => {
const params = [
[payload.uid],
{
password: payload.password,
version: API_VERSION_BACKUP,
},
];

return getCommand({
method: "passwd",
params: params,
});
},
}),
}),
});

Expand Down Expand Up @@ -782,4 +804,5 @@ export const {
useEnableUserMutation,
useDisableUserMutation,
useUnlockUserMutation,
useChangePasswordMutation,
} = api;
Loading