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 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