mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01: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