1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-23 01:16:27 +02:00

feat: add reset password to user manager (#5580)

## About the changes
This PR adds a reset password functionality to user table (only available to admins). This makes it possible to reset passwords without configuring the email service.

![Screenshot from 2023-12-10
20-41-22](https://github.com/Unleash/unleash/assets/32435715/0209525a-4f3a-4998-b9de-7299469e1a68)

![Screenshot from 2023-12-16
16-40-36](https://github.com/Unleash/unleash/assets/32435715/556e324c-c0b0-4bb9-b2b5-3bd653f4d329)

![Screenshot from 2023-12-16
16-40-48](https://github.com/Unleash/unleash/assets/32435715/b0249e9d-9e1a-4cfe-a5ee-0ab22f45ce28)

---------

Co-authored-by: Ivar Conradi Østhus <ivarconr@gmail.com>
Co-authored-by: Thomas Heartman <thomasheartman+github@gmail.com>
This commit is contained in:
Paulo Miranda 2023-12-18 09:54:04 -03:00 committed by GitHub
parent c5b3058890
commit fd34f35e0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 182 additions and 2 deletions

View File

@ -0,0 +1,123 @@
import React, { useState } from 'react';
import classnames from 'classnames';
import { Box, styled, TextField, Typography } from '@mui/material';
import { modalStyles } from 'component/admin/users/util';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { useThemeStyles } from 'themes/themeStyles';
import { IUser } from 'interfaces/user';
import useAdminUsersApi from 'hooks/api/actions/useAdminUsersApi/useAdminUsersApi';
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import { LinkField } from '../../LinkField/LinkField';
const StyledUserAvatar = styled(UserAvatar)(({ theme }) => ({
width: theme.spacing(5),
height: theme.spacing(5),
margin: 0,
}));
interface IChangePasswordProps {
showDialog: boolean;
closeDialog: () => void;
user: IUser;
}
const ResetPassword = ({
showDialog,
closeDialog,
user,
}: IChangePasswordProps) => {
const { classes: themeStyles } = useThemeStyles();
const { resetPassword } = useAdminUsersApi();
const { setToastApiError } = useToast();
const [resetLink, setResetLink] = useState('');
const submit = async (event: React.SyntheticEvent) => {
event.preventDefault();
if (!user.email) {
setToastApiError(
"You can't reset the password of a user who doesn't have an email address.",
);
return;
}
try {
const token = await resetPassword(user.email).then((res) =>
res.ok ? res.json() : undefined,
);
if (token) {
setResetLink(token.resetPasswordUrl);
} else {
setToastApiError(
'Could not reset password. This may be to prevent too many resets. Try again in a minute.',
);
}
} catch (error) {
setToastApiError(formatUnknownError(error));
}
};
const onCancel = (event: React.SyntheticEvent) => {
event.preventDefault();
closeDialog();
};
const closeConfirm = () => {
setResetLink('');
closeDialog();
};
return (
<Dialogue
open={showDialog}
onClick={submit}
style={modalStyles}
onClose={onCancel}
primaryButtonText='Generate Link'
title='Reset password'
secondaryButtonText='Cancel'
maxWidth='xs'
>
<form
onSubmit={submit}
className={classnames(
themeStyles.contentSpacingY,
themeStyles.flexColumn,
)}
>
<Typography variant='subtitle1'>
Resetting password for user
</Typography>
<div className={themeStyles.flexRow}>
<StyledUserAvatar user={user} variant='rounded' />
<Typography
variant='subtitle1'
style={{ marginLeft: '1rem' }}
>
{user.username || user.email}
</Typography>
</div>
</form>
<Dialogue
open={Boolean(resetLink)}
onClick={closeConfirm}
primaryButtonText='Close'
title='Reset password link created'
>
<Box>
<Typography variant='body1'>
The user can use this link to reset their password. You
should not share this link with anyone but the user in
question.
</Typography>
<LinkField inviteLink={resetLink} />
</Box>
</Dialogue>
</Dialogue>
);
};
export default ResetPassword;

View File

@ -1,4 +1,4 @@
import { Delete, Edit, Lock } from '@mui/icons-material';
import { Delete, Edit, Lock, LockReset } from '@mui/icons-material';
import { Box, styled } from '@mui/material';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import { ADMIN } from 'component/providers/AccessProvider/permissions';
@ -12,12 +12,14 @@ const StyledBox = styled(Box)(() => ({
interface IUsersActionsCellProps {
onEdit: (event: React.SyntheticEvent) => void;
onChangePassword: (event: React.SyntheticEvent) => void;
onResetPassword: (event: React.SyntheticEvent) => void;
onDelete: (event: React.SyntheticEvent) => void;
}
export const UsersActionsCell: VFC<IUsersActionsCellProps> = ({
onEdit,
onChangePassword,
onResetPassword,
onDelete,
}) => {
return (
@ -42,6 +44,16 @@ export const UsersActionsCell: VFC<IUsersActionsCellProps> = ({
>
<Lock />
</PermissionIconButton>
<PermissionIconButton
data-loading
onClick={onResetPassword}
permission={ADMIN}
tooltipProps={{
title: 'Reset password',
}}
>
<LockReset />
</PermissionIconButton>
<PermissionIconButton
data-loading
onClick={onDelete}

View File

@ -1,6 +1,7 @@
import React, { useMemo, useState } from 'react';
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
import ChangePassword from './ChangePassword/ChangePassword';
import ResetPassword from './ResetPassword/ResetPassword';
import DeleteUser from './DeleteUser/DeleteUser';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import ConfirmUserAdded from '../ConfirmUserAdded/ConfirmUserAdded';
@ -45,6 +46,12 @@ const UsersList = () => {
const [pwDialog, setPwDialog] = useState<{ open: boolean; user?: IUser }>({
open: false,
});
const [resetPwDialog, setResetPwDialog] = useState<{
open: boolean;
user?: IUser;
}>({
open: false,
});
const { isEnterprise } = useUiConfig();
const [delDialog, setDelDialog] = useState(false);
const [showConfirm, setShowConfirm] = useState(false);
@ -75,10 +82,20 @@ const UsersList = () => {
setPwDialog({ open: true, user });
};
const openResetPwDialog =
(user: IUser) => (e: React.SyntheticEvent<Element, Event>) => {
e.preventDefault();
setResetPwDialog({ open: true, user });
};
const closePwDialog = () => {
setPwDialog({ open: false });
};
const closeResetPwDialog = () => {
setResetPwDialog({ open: false });
};
const onDeleteUser = async (user: IUser) => {
try {
await removeUser(user.id);
@ -180,10 +197,11 @@ const UsersList = () => {
navigate(`/admin/users/${user.id}/edit`);
}}
onChangePassword={openPwDialog(user)}
onResetPassword={openResetPwDialog(user)}
onDelete={openDelDialog(user)}
/>
),
width: 150,
width: 200,
disableSortBy: true,
},
// Always hidden -- for search
@ -334,6 +352,18 @@ const UsersList = () => {
/>
)}
/>
<ConditionallyRender
condition={Boolean(resetPwDialog.user)}
show={() => (
<ResetPassword
showDialog={resetPwDialog.open}
closeDialog={closeResetPwDialog}
user={resetPwDialog.user!}
/>
)}
/>
<ConditionallyRender
condition={Boolean(delUser)}
show={

View File

@ -89,12 +89,27 @@ const useAdminUsersApi = () => {
return makeRequest(req.caller, req.id);
};
const resetPassword = async (email: string) => {
const requestId = 'resetPassword';
const req = createRequest(
'api/admin/user-admin/reset-password',
{
method: 'POST',
body: JSON.stringify({ id: email }),
},
requestId,
);
return makeRequest(req.caller, req.id);
};
return {
addUser,
updateUser,
removeUser,
changePassword,
validatePassword,
resetPassword,
userApiErrors: errors,
userLoading: loading,
};