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.    --------- Co-authored-by: Ivar Conradi Østhus <ivarconr@gmail.com> Co-authored-by: Thomas Heartman <thomasheartman+github@gmail.com>
This commit is contained in:
parent
c5b3058890
commit
fd34f35e0e
@ -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;
|
@ -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}
|
||||
|
@ -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={
|
||||
|
@ -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,
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user