From c88a073a5cdf13cf756a120c153a3d9f3849a32d Mon Sep 17 00:00:00 2001 From: Rishabh Date: Tue, 6 Jun 2023 09:17:54 +0530 Subject: [PATCH] Wired delete and owner actions for staff users in adminX refs https://github.com/TryGhost/Team/issues/3351 - allows owners/admins to delete users based on their permission level - allows admins to be made owner users only if owner is logged in --- .../general/modals/UserDetailModal.tsx | 90 ++++++++++++------- ghost/admin-x-settings/src/utils/api.ts | 14 +++ ghost/admin-x-settings/src/utils/helpers.ts | 4 + 3 files changed, 77 insertions(+), 31 deletions(-) diff --git a/ghost/admin-x-settings/src/components/settings/general/modals/UserDetailModal.tsx b/ghost/admin-x-settings/src/components/settings/general/modals/UserDetailModal.tsx index 33719eb632..cb6d85df3d 100644 --- a/ghost/admin-x-settings/src/components/settings/general/modals/UserDetailModal.tsx +++ b/ghost/admin-x-settings/src/components/settings/general/modals/UserDetailModal.tsx @@ -5,7 +5,7 @@ import Icon from '../../../../admin-x-ds/global/Icon'; import ImageUpload from '../../../../admin-x-ds/global/ImageUpload'; import Menu from '../../../../admin-x-ds/global/Menu'; import Modal from '../../../../admin-x-ds/global/Modal'; -import NiceModal from '@ebay/nice-modal-react'; +import NiceModal, {useModal} from '@ebay/nice-modal-react'; import Radio from '../../../../admin-x-ds/global/Radio'; import React, {useContext, useEffect, useRef, useState} from 'react'; import SettingGroup from '../../../../admin-x-ds/settings/SettingGroup'; @@ -14,8 +14,10 @@ import TextField from '../../../../admin-x-ds/global/TextField'; import Toggle from '../../../../admin-x-ds/global/Toggle'; import useRoles from '../../../../hooks/useRoles'; import {FileService, ServicesContext} from '../../../providers/ServiceProvider'; +import {MenuItem} from '../../../../admin-x-ds/global/Menu'; import {User} from '../../../../types/api'; -import {isOwnerUser} from '../../../../utils/helpers'; +import {isAdminUser, isOwnerUser} from '../../../../utils/helpers'; +import {showToast} from '../../../../admin-x-ds/global/Toast'; interface CustomHeadingProps { children?: React.ReactNode; @@ -393,34 +395,12 @@ const UserMenuTrigger = () => ( ); -const confirmMakeOwner = () => { - NiceModal.show(ConfirmationModal, { - title: 'Transfer Ownership', - prompt: 'Are you sure you want to transfer the ownership of this blog? You will not be able to undo this action.', - okLabel: 'Yep — I\'m sure', - okColor: 'red' - }); -}; - -const confirmDelete = () => { - NiceModal.show(ConfirmationModal, { - title: 'Are you sure you want to delete this user?', - prompt: ( - <> -

The [user] will be permanently deleted and all their posts will be automatically assigned to the [site owner name].

-

To make these easy to find in the future, each post will be given an internal tag of [new internal tag with username]

- - ), - okLabel: 'Delete user', - okColor: 'red' - }); -}; - const UserDetailModal:React.FC = ({user, updateUser}) => { const {api} = useContext(ServicesContext); const [userData, setUserData] = useState(user); const [saveState, setSaveState] = useState(''); const {fileService} = useContext(ServicesContext) as {fileService: FileService}; + const mainModal = useModal(); const confirmSuspend = (_user: User) => { let warningText = 'This user will no longer be able to log in but their posts will be kept.'; @@ -447,6 +427,46 @@ const UserDetailModal:React.FC = ({user, updateUser}) => { }); }; + const confirmDelete = (_user: User) => { + NiceModal.show(ConfirmationModal, { + title: 'Are you sure you want to delete this user?', + prompt: ( + <> +

The [user] will be permanently deleted and all their posts will be automatically assigned to the [site owner name].

+

To make these easy to find in the future, each post will be given an internal tag of [new internal tag with username]

+ + ), + okLabel: 'Delete user', + okColor: 'red', + onOk: async (modal) => { + await api.users.delete(_user?.id); + modal?.remove(); + mainModal?.remove(); + showToast({ + message: 'User deleted', + type: 'success' + }); + } + }); + }; + + const confirmMakeOwner = () => { + NiceModal.show(ConfirmationModal, { + title: 'Transfer Ownership', + prompt: 'Are you sure you want to transfer the ownership of this blog? You will not be able to undo this action.', + okLabel: 'Yep — I\'m sure', + okColor: 'red', + onOk: async (modal) => { + await api.users.makeOwner(user.id); + modal?.remove(); + showToast({ + message: 'Ownership transferred', + type: 'success' + }); + } + }); + }; + const handleImageUpload = async (image: string, file: File) => { try { const imageUrl = await fileService.uploadImage(file); @@ -477,17 +497,22 @@ const UserDetailModal:React.FC = ({user, updateUser}) => { let suspendUserLabel = user?.status === 'inactive' ? 'Un-suspend user' : 'Suspend user'; - const menuItems = [ - { + let menuItems: MenuItem[] = []; + + if (isAdminUser(user)) { + menuItems.push({ id: 'make-owner', label: 'Make owner', onClick: confirmMakeOwner - }, + }); + } + + menuItems = menuItems.concat([ { id: 'delete-user', label: 'Delete user', onClick: () => { - confirmDelete(); + confirmDelete(user); } }, { @@ -499,9 +524,12 @@ const UserDetailModal:React.FC = ({user, updateUser}) => { }, { id: 'view-user-activity', - label: 'View user activity' + label: 'View user activity', + onClick: () => { + // TODO: show user activity + } } - ]; + ]); let okLabel = saveState === 'saved' ? 'Saved' : 'Save'; if (saveState === 'saving') { diff --git a/ghost/admin-x-settings/src/utils/api.ts b/ghost/admin-x-settings/src/utils/api.ts index a8d1f45d86..e7e775e025 100644 --- a/ghost/admin-x-settings/src/utils/api.ts +++ b/ghost/admin-x-settings/src/utils/api.ts @@ -98,6 +98,7 @@ interface API { edit: (editedUser: User) => Promise; delete: (userId: string) => Promise; updatePassword: (options: UpdatePasswordOptions) => Promise; + makeOwner: (userId: string) => Promise; }; roles: { browse: (options?: BrowseRoleOptions) => Promise; @@ -219,6 +220,19 @@ function setupGhostApi({ghostVersion}: GhostApiOptions): API { }); const data: DeleteUserResponse = await response.json(); return data; + }, + makeOwner: async (userId: string) => { + const payload = JSON.stringify({ + owner: [{ + id: userId + }] + }); + const response = await fetcher(`/users/owner/`, { + method: 'PUT', + body: payload + }); + const data: UsersResponseType = await response.json(); + return data; } }, roles: { diff --git a/ghost/admin-x-settings/src/utils/helpers.ts b/ghost/admin-x-settings/src/utils/helpers.ts index f1fd11241b..c80627675c 100644 --- a/ghost/admin-x-settings/src/utils/helpers.ts +++ b/ghost/admin-x-settings/src/utils/helpers.ts @@ -62,3 +62,7 @@ export function generateAvatarColor(name: string) { export function isOwnerUser(user: User) { return user.roles.some(role => role.name === 'Owner'); } + +export function isAdminUser(user: User) { + return user.roles.some(role => role.name === 'Administrator'); +}