🚫👤feat: delete user from UI (#1526)

* initial commit

* fix: UserController bugs; fix: lint errors

* fix: delete files

* language support

* style(DeleteAccount): update to the latest style

* style: fix after merge main

* chore: Add canDeleteAccount middleware for user deletion endpoint

* chore: renamed to ALLOW_ACCOUNT_DELETION

* fix(canDeleteAccount): use uppercase admin role

* chore: imports order

* chore: Enable account deletion by default if omitted/commented out

* chore: Add logging for user account deletion

* chore: Bump data-provider package version to 0.6.6

* chore: Import Transaction model in UserController

* chore: Update CONFIG_VERSION to 1.1.4

* chore: Update user account deletion logging

* chore: Refactor user account deletion logic

---------

Co-authored-by: Berry-13 <root@Berry>
Co-authored-by: Danny Avila <messagedaniel@protonmail.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Marco Beretta 2024-06-06 01:35:12 +02:00 committed by GitHub
parent f69b317171
commit a7f5b57272
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 348 additions and 17 deletions

View file

@ -15,7 +15,9 @@ const SocialButton = ({ id, enabled, serverDomain, oauthPath, Icon, label }) =>
const handleMouseLeave = () => {
setIsHovered(false);
if (isPressed) {setIsPressed(false);}
if (isPressed) {
setIsPressed(false);
}
};
const handleMouseDown = () => {

View file

@ -15,7 +15,6 @@ const ClearConvos = ({ open, onOpenChange }) => {
// Clear all conversations
const clearConvos = () => {
if (confirmClear) {
console.log('Clearing conversations...');
clearConvosMutation.mutate(
{},
{

View file

@ -2,6 +2,7 @@ import React from 'react';
import { useRecoilState } from 'recoil';
import * as Tabs from '@radix-ui/react-tabs';
import { SettingsTabValues } from 'librechat-data-provider';
import DeleteAccount from './DeleteAccount';
import { Switch } from '~/components/ui';
import { useLocalize } from '~/hooks';
import Avatar from './Avatar';
@ -28,6 +29,9 @@ function Account({ onCheckedChange }: { onCheckedChange?: (value: boolean) => vo
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<Avatar />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<DeleteAccount />
</div>
<div className="flex items-center justify-between">
<div> {localize('com_nav_user_name_display')} </div>
<Switch
@ -39,7 +43,6 @@ function Account({ onCheckedChange }: { onCheckedChange?: (value: boolean) => vo
/>
</div>
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600"></div>
</Tabs.Content>
);
}

View file

@ -0,0 +1,174 @@
import React, { useState, useCallback } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogButton,
Input,
} from '~/components/ui';
import { cn, defaultTextProps, removeFocusOutlines } from '~/utils';
import { useDeleteUserMutation } from '~/data-provider';
import { Spinner, LockIcon } from '~/components/svg';
import { useAuthContext } from '~/hooks/AuthContext';
import { useLocalize } from '~/hooks';
const DeleteAccount = ({ disabled = false }: { title?: string; disabled?: boolean }) => {
const localize = useLocalize();
const { user, logout } = useAuthContext();
const { mutate: deleteUser, isLoading: isDeleting } = useDeleteUserMutation({
onSuccess: () => logout(),
});
const [isDialogOpen, setDialogOpen] = useState<boolean>(false);
const [deleteInput, setDeleteInput] = useState('');
const [emailInput, setEmailInput] = useState('');
const [isLocked, setIsLocked] = useState(true);
const onClick = useCallback(() => {
setDialogOpen(true);
}, []);
const handleDeleteUser = () => {
if (!isLocked) {
deleteUser(undefined);
}
};
const handleInputChange = useCallback(
(newEmailInput: string, newDeleteInput: string) => {
const isEmailCorrect =
newEmailInput.trim().toLowerCase() === user?.email?.trim().toLowerCase();
const isDeleteInputCorrect = newDeleteInput === 'DELETE';
setIsLocked(!(isEmailCorrect && isDeleteInputCorrect));
},
[user?.email],
);
return (
<>
<div className="flex items-center justify-between">
<span>{localize('com_nav_delete_account')}</span>
<label>
<DialogButton
id={'delete-user-account'}
disabled={disabled}
onClick={onClick}
className={cn(
'btn btn-danger relative border-none bg-red-700 text-white hover:bg-red-800 dark:hover:bg-red-800',
)}
>
{localize('com_ui_delete')}
</DialogButton>
</label>
</div>
<Dialog open={isDialogOpen} onOpenChange={() => setDialogOpen(false)}>
<DialogContent
className={cn('shadow-2xl md:h-[500px] md:w-[450px]')}
style={{ borderRadius: '12px', padding: '20px' }}
>
<DialogHeader>
<DialogTitle className="text-lg font-medium leading-6">
{localize('com_nav_delete_account_confirm')}
</DialogTitle>
</DialogHeader>
<div className="mb-20 text-sm text-black dark:text-white">
<ul>
<li>{localize('com_nav_delete_warning')}</li>
<li>{localize('com_nav_delete_data_info')}</li>
<li>{localize('com_nav_delete_help_center')}</li>
</ul>
</div>
<div className="flex-col items-center justify-center">
<div className="mb-4">
{renderInput(
localize('com_nav_delete_account_email_placeholder'),
'email-confirm-input',
user?.email || '',
(e) => {
setEmailInput(e.target.value);
handleInputChange(e.target.value, deleteInput);
},
)}
</div>
<div className="mb-4">
{renderInput(
localize('com_nav_delete_account_confirm_placeholder'),
'delete-confirm-input',
'',
(e) => {
setDeleteInput(e.target.value);
handleInputChange(emailInput, e.target.value);
},
)}
</div>
{renderDeleteButton(handleDeleteUser, isDeleting, isLocked, localize)}
</div>
</DialogContent>
</Dialog>
</>
);
};
const renderInput = (
label: string,
id: string,
value: string,
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void,
) => (
<div className="mb-4">
<label className="mb-1 block text-sm font-medium text-black dark:text-white">{label}</label>
<Input
id={id}
onChange={onChange}
placeholder={value}
className={cn(
defaultTextProps,
'h-10 max-h-10 w-full max-w-full rounded-md bg-white px-3 py-2',
removeFocusOutlines,
)}
/>
</div>
);
const renderDeleteButton = (
handleDeleteUser: () => void,
isDeleting: boolean,
isLocked: boolean,
localize: (key: string) => string,
) => (
<button
className={cn(
'mt-4 flex w-full items-center justify-center rounded-lg px-4 py-2 transition-colors duration-200',
isLocked
? 'cursor-not-allowed bg-gray-200 text-gray-300 dark:bg-gray-500 dark:text-gray-600'
: isDeleting
? 'cursor-not-allowed bg-gray-100 text-gray-700 dark:bg-gray-400 dark:text-gray-700'
: 'bg-red-700 text-white hover:bg-red-800 ',
)}
onClick={handleDeleteUser}
disabled={isDeleting || isLocked}
>
{isDeleting ? (
<div className="flex h-6 justify-center">
<Spinner className="icon-sm m-auto" />
</div>
) : (
<>
{isLocked ? (
<>
<LockIcon />
<span className="ml-2">{localize('com_ui_locked')}</span>
</>
) : (
<>
<LockIcon />
<span className="ml-2">{localize('com_nav_delete_account_button')}</span>
</>
)}
</>
)}
</button>
);
export default DeleteAccount;

View file

@ -0,0 +1,19 @@
export default function LockIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="lucide lucide-lock"
>
<rect width="18" height="11" x="3" y="11" rx="2" ry="2" />
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
</svg>
);
}

View file

@ -46,6 +46,7 @@ export { default as VolumeIcon } from './VolumeIcon';
export { default as VolumeMuteIcon } from './VolumeMuteIcon';
export { default as SendMessageIcon } from './SendMessageIcon';
export { default as UserIcon } from './UserIcon';
export { default as LockIcon } from './LockIcon';
export { default as NewChatIcon } from './NewChatIcon';
export { default as ExperimentIcon } from './ExperimentIcon';
export { default as GoogleIconChat } from './GoogleIconChat';