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 { Box, styled } from '@mui/material';
|
||||||
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||||
@ -12,12 +12,14 @@ const StyledBox = styled(Box)(() => ({
|
|||||||
interface IUsersActionsCellProps {
|
interface IUsersActionsCellProps {
|
||||||
onEdit: (event: React.SyntheticEvent) => void;
|
onEdit: (event: React.SyntheticEvent) => void;
|
||||||
onChangePassword: (event: React.SyntheticEvent) => void;
|
onChangePassword: (event: React.SyntheticEvent) => void;
|
||||||
|
onResetPassword: (event: React.SyntheticEvent) => void;
|
||||||
onDelete: (event: React.SyntheticEvent) => void;
|
onDelete: (event: React.SyntheticEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UsersActionsCell: VFC<IUsersActionsCellProps> = ({
|
export const UsersActionsCell: VFC<IUsersActionsCellProps> = ({
|
||||||
onEdit,
|
onEdit,
|
||||||
onChangePassword,
|
onChangePassword,
|
||||||
|
onResetPassword,
|
||||||
onDelete,
|
onDelete,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
@ -42,6 +44,16 @@ export const UsersActionsCell: VFC<IUsersActionsCellProps> = ({
|
|||||||
>
|
>
|
||||||
<Lock />
|
<Lock />
|
||||||
</PermissionIconButton>
|
</PermissionIconButton>
|
||||||
|
<PermissionIconButton
|
||||||
|
data-loading
|
||||||
|
onClick={onResetPassword}
|
||||||
|
permission={ADMIN}
|
||||||
|
tooltipProps={{
|
||||||
|
title: 'Reset password',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LockReset />
|
||||||
|
</PermissionIconButton>
|
||||||
<PermissionIconButton
|
<PermissionIconButton
|
||||||
data-loading
|
data-loading
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
||||||
import ChangePassword from './ChangePassword/ChangePassword';
|
import ChangePassword from './ChangePassword/ChangePassword';
|
||||||
|
import ResetPassword from './ResetPassword/ResetPassword';
|
||||||
import DeleteUser from './DeleteUser/DeleteUser';
|
import DeleteUser from './DeleteUser/DeleteUser';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import ConfirmUserAdded from '../ConfirmUserAdded/ConfirmUserAdded';
|
import ConfirmUserAdded from '../ConfirmUserAdded/ConfirmUserAdded';
|
||||||
@ -45,6 +46,12 @@ const UsersList = () => {
|
|||||||
const [pwDialog, setPwDialog] = useState<{ open: boolean; user?: IUser }>({
|
const [pwDialog, setPwDialog] = useState<{ open: boolean; user?: IUser }>({
|
||||||
open: false,
|
open: false,
|
||||||
});
|
});
|
||||||
|
const [resetPwDialog, setResetPwDialog] = useState<{
|
||||||
|
open: boolean;
|
||||||
|
user?: IUser;
|
||||||
|
}>({
|
||||||
|
open: false,
|
||||||
|
});
|
||||||
const { isEnterprise } = useUiConfig();
|
const { isEnterprise } = useUiConfig();
|
||||||
const [delDialog, setDelDialog] = useState(false);
|
const [delDialog, setDelDialog] = useState(false);
|
||||||
const [showConfirm, setShowConfirm] = useState(false);
|
const [showConfirm, setShowConfirm] = useState(false);
|
||||||
@ -75,10 +82,20 @@ const UsersList = () => {
|
|||||||
setPwDialog({ open: true, user });
|
setPwDialog({ open: true, user });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openResetPwDialog =
|
||||||
|
(user: IUser) => (e: React.SyntheticEvent<Element, Event>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setResetPwDialog({ open: true, user });
|
||||||
|
};
|
||||||
|
|
||||||
const closePwDialog = () => {
|
const closePwDialog = () => {
|
||||||
setPwDialog({ open: false });
|
setPwDialog({ open: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const closeResetPwDialog = () => {
|
||||||
|
setResetPwDialog({ open: false });
|
||||||
|
};
|
||||||
|
|
||||||
const onDeleteUser = async (user: IUser) => {
|
const onDeleteUser = async (user: IUser) => {
|
||||||
try {
|
try {
|
||||||
await removeUser(user.id);
|
await removeUser(user.id);
|
||||||
@ -180,10 +197,11 @@ const UsersList = () => {
|
|||||||
navigate(`/admin/users/${user.id}/edit`);
|
navigate(`/admin/users/${user.id}/edit`);
|
||||||
}}
|
}}
|
||||||
onChangePassword={openPwDialog(user)}
|
onChangePassword={openPwDialog(user)}
|
||||||
|
onResetPassword={openResetPwDialog(user)}
|
||||||
onDelete={openDelDialog(user)}
|
onDelete={openDelDialog(user)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
width: 150,
|
width: 200,
|
||||||
disableSortBy: true,
|
disableSortBy: true,
|
||||||
},
|
},
|
||||||
// Always hidden -- for search
|
// 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
|
<ConditionallyRender
|
||||||
condition={Boolean(delUser)}
|
condition={Boolean(delUser)}
|
||||||
show={
|
show={
|
||||||
|
@ -89,12 +89,27 @@ const useAdminUsersApi = () => {
|
|||||||
return makeRequest(req.caller, req.id);
|
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 {
|
return {
|
||||||
addUser,
|
addUser,
|
||||||
updateUser,
|
updateUser,
|
||||||
removeUser,
|
removeUser,
|
||||||
changePassword,
|
changePassword,
|
||||||
validatePassword,
|
validatePassword,
|
||||||
|
resetPassword,
|
||||||
userApiErrors: errors,
|
userApiErrors: errors,
|
||||||
userLoading: loading,
|
userLoading: loading,
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user