From e96302ccb2dd08e4d1d7d8d3369e7b5c4828af28 Mon Sep 17 00:00:00 2001 From: JimmFly Date: Thu, 13 Mar 2025 18:37:21 +0800 Subject: [PATCH] feat(admin): add ban user to admin panel (#10780) --- .../modules/accounts/components/columns.tsx | 22 +++- .../components/data-table-row-actions.tsx | 100 +++++++++++++++--- .../accounts/components/delete-account.tsx | 1 + .../accounts/components/disable-account.tsx | 77 ++++++++++++++ .../accounts/components/enable-account.tsx | 48 +++++++++ .../components/use-user-management.ts | 51 +++++++++ .../admin/src/modules/nav/user-dropdown.tsx | 13 ++- .../graphql/src/graphql/disable-user.gql | 6 ++ .../graphql/src/graphql/enable-user.gql | 6 ++ .../frontend/graphql/src/graphql/index.ts | 23 ++++ .../graphql/src/graphql/list-users.gql | 1 + packages/frontend/graphql/src/schema.ts | 29 +++++ 12 files changed, 354 insertions(+), 23 deletions(-) create mode 100644 packages/frontend/admin/src/modules/accounts/components/disable-account.tsx create mode 100644 packages/frontend/admin/src/modules/accounts/components/enable-account.tsx create mode 100644 packages/frontend/graphql/src/graphql/disable-user.gql create mode 100644 packages/frontend/graphql/src/graphql/enable-user.gql diff --git a/packages/frontend/admin/src/modules/accounts/components/columns.tsx b/packages/frontend/admin/src/modules/accounts/components/columns.tsx index 0f562ddce9..7ad64ff58c 100644 --- a/packages/frontend/admin/src/modules/accounts/components/columns.tsx +++ b/packages/frontend/admin/src/modules/accounts/components/columns.tsx @@ -6,6 +6,7 @@ import { import type { UserType } from '@affine/graphql'; import { FeatureType } from '@affine/graphql'; import type { ColumnDef } from '@tanstack/react-table'; +import { cssVarV2 } from '@toeverything/theme/v2'; import clsx from 'clsx'; import { LockIcon, @@ -64,18 +65,31 @@ export const columns: ColumnDef[] = [
- {row.original.name}{' '} + {row.original.name} {row.original.features.includes(FeatureType.Admin) && ( Admin )} + {row.original.disabled && ( + + Disabled + + )}
{row.original.email} diff --git a/packages/frontend/admin/src/modules/accounts/components/data-table-row-actions.tsx b/packages/frontend/admin/src/modules/accounts/components/data-table-row-actions.tsx index 89e9fe9914..170ac091c4 100644 --- a/packages/frontend/admin/src/modules/accounts/components/data-table-row-actions.tsx +++ b/packages/frontend/admin/src/modules/accounts/components/data-table-row-actions.tsx @@ -7,20 +7,28 @@ import { DropdownMenuTrigger, } from '@affine/admin/components/ui/dropdown-menu'; import { + AccountBanIcon, + DeleteIcon, + EditIcon, LockIcon, - MoreVerticalIcon, - SettingsIcon, - TrashIcon, -} from 'lucide-react'; + MoreHorizontalIcon, +} from '@blocksuite/icons/rc'; import { useCallback, useState } from 'react'; import { toast } from 'sonner'; import { useRightPanel } from '../../panel/context'; import type { UserType } from '../schema'; import { DeleteAccountDialog } from './delete-account'; +import { DisableAccountDialog } from './disable-account'; import { DiscardChanges } from './discard-changes'; +import { EnableAccountDialog } from './enable-account'; import { ResetPasswordDialog } from './reset-password'; -import { useDeleteUser, useResetUserPassword } from './use-user-management'; +import { + useDeleteUser, + useDisableUser, + useEnableUser, + useResetUserPassword, +} from './use-user-management'; import { UpdateUserForm } from './user-form'; interface DataTableRowActionsProps { @@ -30,10 +38,14 @@ interface DataTableRowActionsProps { export function DataTableRowActions({ user }: DataTableRowActionsProps) { const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [resetPasswordDialogOpen, setResetPasswordDialogOpen] = useState(false); + const [disableDialogOpen, setDisableDialogOpen] = useState(false); + const [enableDialogOpen, setEnableDialogOpen] = useState(false); const [discardDialogOpen, setDiscardDialogOpen] = useState(false); const { openPanel, isOpen, closePanel, setPanelContent } = useRightPanel(); const deleteUser = useDeleteUser(); + const disableUser = useDisableUser(); + const enableUser = useEnableUser(); const { resetPasswordLink, onResetPassword } = useResetUserPassword(); const openResetPasswordDialog = useCallback(() => { @@ -56,25 +68,56 @@ export function DataTableRowActions({ user }: DataTableRowActionsProps) { }); }, [resetPasswordLink]); - const onDeleting = useCallback(() => { + const handleDeleting = useCallback(() => { if (isOpen) { closePanel(); } setDeleteDialogOpen(false); }, [closePanel, isOpen]); + const handleDisabling = useCallback(() => { + if (isOpen) { + closePanel(); + } + setDisableDialogOpen(false); + }, [closePanel, isOpen]); + const handleEnabling = useCallback(() => { + if (isOpen) { + closePanel(); + } + setEnableDialogOpen(false); + }, [closePanel, isOpen]); const handleDelete = useCallback(() => { - deleteUser(user.id, onDeleting); - }, [deleteUser, onDeleting, user.id]); + deleteUser(user.id, handleDeleting); + }, [deleteUser, handleDeleting, user.id]); + const handleDisable = useCallback(() => { + disableUser(user.id, handleDisabling); + }, [disableUser, handleDisabling, user.id]); + const handleEnable = useCallback(() => { + enableUser(user.id, handleEnabling); + }, [enableUser, handleEnabling, user.id]); const openDeleteDialog = useCallback(() => { setDeleteDialogOpen(true); }, []); - const closeDeleteDialog = useCallback(() => { setDeleteDialogOpen(false); }, []); + const openDisableDialog = useCallback(() => { + setDisableDialogOpen(true); + }, []); + const closeDisableDialog = useCallback(() => { + setDisableDialogOpen(false); + }, []); + + const openEnableDialog = useCallback(() => { + setEnableDialogOpen(true); + }, []); + const closeEnableDialog = useCallback(() => { + setEnableDialogOpen(false); + }, []); + const handleDiscardChangesCancel = useCallback(() => { setDiscardDialogOpen(false); }, []); @@ -122,7 +165,7 @@ export function DataTableRowActions({ user }: DataTableRowActionsProps) { variant="ghost" className="flex h-8 w-8 p-0 data-[state=open]:bg-muted" > - + Open menu @@ -135,21 +178,36 @@ export function DataTableRowActions({ user }: DataTableRowActionsProps) { className="px-2 py-[6px] text-sm font-medium gap-2 cursor-pointer" onSelect={openResetPasswordDialog} > - Reset Password + Reset Password - Edit + Edit - + {user.disabled && ( + + Enable Email + + )} + {!user.disabled && ( + + Disable & Delete data + + )} - Delete + Delete @@ -160,6 +218,20 @@ export function DataTableRowActions({ user }: DataTableRowActionsProps) { onOpenChange={setDeleteDialogOpen} onDelete={handleDelete} /> + + Delete diff --git a/packages/frontend/admin/src/modules/accounts/components/disable-account.tsx b/packages/frontend/admin/src/modules/accounts/components/disable-account.tsx new file mode 100644 index 0000000000..19c92702ec --- /dev/null +++ b/packages/frontend/admin/src/modules/accounts/components/disable-account.tsx @@ -0,0 +1,77 @@ +import { Button } from '@affine/admin/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@affine/admin/components/ui/dialog'; +import { Input } from '@affine/admin/components/ui/input'; +import { useCallback, useEffect, useState } from 'react'; + +export const DisableAccountDialog = ({ + email, + open, + onClose, + onDisable, + onOpenChange, +}: { + email: string; + open: boolean; + onClose: () => void; + onDisable: () => void; + onOpenChange: (open: boolean) => void; +}) => { + const [input, setInput] = useState(''); + const handleInput = useCallback( + (event: React.ChangeEvent) => { + setInput(event.target.value); + }, + [setInput] + ); + + useEffect(() => { + if (!open) { + setInput(''); + } + }, [open]); + + return ( + + + + Disable Account ? + + The data associated with {email}{' '} + will be deleted and cannot be used for logging in. This operation is + irreversible. Please proceed with caution. + + + + +
+ + +
+
+
+
+ ); +}; diff --git a/packages/frontend/admin/src/modules/accounts/components/enable-account.tsx b/packages/frontend/admin/src/modules/accounts/components/enable-account.tsx new file mode 100644 index 0000000000..9ca8422777 --- /dev/null +++ b/packages/frontend/admin/src/modules/accounts/components/enable-account.tsx @@ -0,0 +1,48 @@ +import { Button } from '@affine/admin/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@affine/admin/components/ui/dialog'; + +export const EnableAccountDialog = ({ + open, + email, + onClose, + onConfirm, + onOpenChange, +}: { + open: boolean; + email: string; + onClose: () => void; + onConfirm: () => void; + onOpenChange: (open: boolean) => void; +}) => { + return ( + + + + Enable Account + + Are you sure you want to enable the account? After enabling the + account, the {email} email can be + used to log in. + + + +
+ + +
+
+
+
+ ); +}; diff --git a/packages/frontend/admin/src/modules/accounts/components/use-user-management.ts b/packages/frontend/admin/src/modules/accounts/components/use-user-management.ts index 94ee0bcca5..39a76b1482 100644 --- a/packages/frontend/admin/src/modules/accounts/components/use-user-management.ts +++ b/packages/frontend/admin/src/modules/accounts/components/use-user-management.ts @@ -8,6 +8,8 @@ import { createChangePasswordUrlMutation, createUserMutation, deleteUserMutation, + disableUserMutation, + enableUserMutation, getUsersCountQuery, listUsersQuery, updateAccountFeaturesMutation, @@ -162,6 +164,55 @@ export const useDeleteUser = () => { return deleteById; }; +export const useEnableUser = () => { + const { trigger: enableUserById } = useMutation({ + mutation: enableUserMutation, + }); + + const revalidate = useMutateQueryResource(); + + const enableById = useAsyncCallback( + async (id: string, callback?: () => void) => { + await enableUserById({ id }) + .then(async ({ enableUser }) => { + await revalidate(listUsersQuery); + toast(`User ${enableUser.email} enabled successfully`); + callback?.(); + }) + .catch(e => { + toast.error('Failed to enable user: ' + e.message); + }); + }, + [enableUserById, revalidate] + ); + + return enableById; +}; +export const useDisableUser = () => { + const { trigger: disableUserById } = useMutation({ + mutation: disableUserMutation, + }); + + const revalidate = useMutateQueryResource(); + + const disableById = useAsyncCallback( + async (id: string, callback?: () => void) => { + await disableUserById({ id }) + .then(async ({ banUser }) => { + await revalidate(listUsersQuery); + toast(`User ${banUser.email} disabled successfully`); + callback?.(); + }) + .catch(e => { + toast.error('Failed to disable user: ' + e.message); + }); + }, + [disableUserById, revalidate] + ); + + return disableById; +}; + export const useUserCount = () => { const { data: { usersCount }, diff --git a/packages/frontend/admin/src/modules/nav/user-dropdown.tsx b/packages/frontend/admin/src/modules/nav/user-dropdown.tsx index 8da2ff5a6e..db5d8296ca 100644 --- a/packages/frontend/admin/src/modules/nav/user-dropdown.tsx +++ b/packages/frontend/admin/src/modules/nav/user-dropdown.tsx @@ -12,7 +12,9 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@affine/admin/components/ui/dropdown-menu'; -import { CircleUser, MoreVertical } from 'lucide-react'; +import { MoreVerticalIcon } from '@blocksuite/icons/rc'; +import { cssVarV2 } from '@toeverything/theme/v2'; +import { CircleUser } from 'lucide-react'; import { useCallback } from 'react'; import { toast } from 'sonner'; @@ -79,10 +81,11 @@ export function UserDropdown({ isCollapsed }: UserDropdownProps) { {currentUser?.email.split('@')[0]} )} Admin @@ -91,7 +94,7 @@ export function UserDropdown({ isCollapsed }: UserDropdownProps) { diff --git a/packages/frontend/graphql/src/graphql/disable-user.gql b/packages/frontend/graphql/src/graphql/disable-user.gql new file mode 100644 index 0000000000..695c4ffd6c --- /dev/null +++ b/packages/frontend/graphql/src/graphql/disable-user.gql @@ -0,0 +1,6 @@ +mutation disableUser($id: String!) { + banUser(id: $id) { + email + disabled + } +} diff --git a/packages/frontend/graphql/src/graphql/enable-user.gql b/packages/frontend/graphql/src/graphql/enable-user.gql new file mode 100644 index 0000000000..e3673ab698 --- /dev/null +++ b/packages/frontend/graphql/src/graphql/enable-user.gql @@ -0,0 +1,6 @@ +mutation enableUser($id: String!) { + enableUser(id: $id) { + email + disabled + } +} diff --git a/packages/frontend/graphql/src/graphql/index.ts b/packages/frontend/graphql/src/graphql/index.ts index c9cbe4dc36..ee179924c2 100644 --- a/packages/frontend/graphql/src/graphql/index.ts +++ b/packages/frontend/graphql/src/graphql/index.ts @@ -439,6 +439,17 @@ export const deleteWorkspaceMutation = { }`, }; +export const disableUserMutation = { + id: 'disableUserMutation' as const, + op: 'disableUser', + query: `mutation disableUser($id: String!) { + banUser(id: $id) { + email + disabled + } +}`, +}; + export const getDocRolePermissionsQuery = { id: 'getDocRolePermissionsQuery' as const, op: 'getDocRolePermissions', @@ -465,6 +476,17 @@ export const getDocRolePermissionsQuery = { }`, }; +export const enableUserMutation = { + id: 'enableUserMutation' as const, + op: 'enableUser', + query: `mutation enableUser($id: String!) { + enableUser(id: $id) { + email + disabled + } +}`, +}; + export const generateLicenseKeyMutation = { id: 'generateLicenseKeyMutation' as const, op: 'generateLicenseKey', @@ -970,6 +992,7 @@ export const listUsersQuery = { id name email + disabled features hasPassword emailVerified diff --git a/packages/frontend/graphql/src/graphql/list-users.gql b/packages/frontend/graphql/src/graphql/list-users.gql index 9e96d08558..d1fb23d48b 100644 --- a/packages/frontend/graphql/src/graphql/list-users.gql +++ b/packages/frontend/graphql/src/graphql/list-users.gql @@ -3,6 +3,7 @@ query listUsers($filter: ListUserInput!) { id name email + disabled features hasPassword emailVerified diff --git a/packages/frontend/graphql/src/schema.ts b/packages/frontend/graphql/src/schema.ts index 189817ec72..14b620d0ce 100644 --- a/packages/frontend/graphql/src/schema.ts +++ b/packages/frontend/graphql/src/schema.ts @@ -2597,6 +2597,15 @@ export type DeleteWorkspaceMutation = { deleteWorkspace: boolean; }; +export type DisableUserMutationVariables = Exact<{ + id: Scalars['String']['input']; +}>; + +export type DisableUserMutation = { + __typename?: 'Mutation'; + banUser: { __typename?: 'UserType'; email: string; disabled: boolean }; +}; + export type GetDocRolePermissionsQueryVariables = Exact<{ workspaceId: Scalars['String']['input']; docId: Scalars['String']['input']; @@ -2628,6 +2637,15 @@ export type GetDocRolePermissionsQuery = { }; }; +export type EnableUserMutationVariables = Exact<{ + id: Scalars['String']['input']; +}>; + +export type EnableUserMutation = { + __typename?: 'Mutation'; + enableUser: { __typename?: 'UserType'; email: string; disabled: boolean }; +}; + export type CredentialsRequirementsFragment = { __typename?: 'CredentialsRequirementType'; password: { @@ -3195,6 +3213,7 @@ export type ListUsersQuery = { id: string; name: string; email: string; + disabled: boolean; features: Array; hasPassword: boolean | null; emailVerified: boolean; @@ -4152,6 +4171,16 @@ export type Mutations = variables: DeleteWorkspaceMutationVariables; response: DeleteWorkspaceMutation; } + | { + name: 'disableUserMutation'; + variables: DisableUserMutationVariables; + response: DisableUserMutation; + } + | { + name: 'enableUserMutation'; + variables: EnableUserMutationVariables; + response: EnableUserMutation; + } | { name: 'generateLicenseKeyMutation'; variables: GenerateLicenseKeyMutationVariables;