diff --git a/packages/api/src/components/AppContextProvider.ts b/packages/api/src/components/AppContextProvider.ts index 7ebe31746..c86db2dc5 100644 --- a/packages/api/src/components/AppContextProvider.ts +++ b/packages/api/src/components/AppContextProvider.ts @@ -32,7 +32,7 @@ import { InitiatePasswordResetInteractor } from '~interactors/InitiatePasswordRe import { ResetUserPasswordInteractor } from '~interactors/ResetUserPasswordInteractor' import { SetUserPasswordInteractor } from '~interactors/SetUserPasswordInteractor' import { CreateNewUserInteractor } from '~interactors/CreateNewUserInteractor' -import { DeleteUserInteractor } from '~interactors/DeleteUserInteractor' +import { RemoveUserFromOrganizationInteractor } from '~interactors/RemoveUserFromOrganizationInteractor' import { UpdateUserInteractor } from '~interactors/UpdateUserInteractor' import { UpdateUserFCMTokenInteractor } from '~interactors/UpdateUserFCMTokenInteractor' import { MarkMentionSeenInteractor } from '~interactors/MarkMentionSeenInteractor' @@ -69,8 +69,7 @@ import { InputEntityToOrgIdStrategy, InputServiceAnswerEntityToOrgIdStrategy, OrganizationSrcStrategy, - OrgIdArgStrategy, - UserWithinOrgStrategy + OrgIdArgStrategy } from './orgAuthStrategies' const logger = createLogger('app-context-provider') @@ -112,8 +111,7 @@ export class AppContextProvider implements AsyncProvider { new OrgIdArgStrategy(authenticator), new EntityIdToOrgIdStrategy(authenticator), new InputEntityToOrgIdStrategy(authenticator), - new InputServiceAnswerEntityToOrgIdStrategy(authenticator), - new UserWithinOrgStrategy(authenticator) + new InputServiceAnswerEntityToOrgIdStrategy(authenticator) ] return { @@ -214,7 +212,11 @@ export class AppContextProvider implements AsyncProvider { ), setUserPassword: new SetUserPasswordInteractor(localization, userCollection), createNewUser: new CreateNewUserInteractor(localization, mailer, userCollection, config), - deleteUser: new DeleteUserInteractor(localization, userCollection, engagementCollection), + removeUserFromOrganization: new RemoveUserFromOrganizationInteractor( + localization, + userCollection, + engagementCollection + ), updateUser: new UpdateUserInteractor(localization, userCollection), updateUserFCMToken: new UpdateUserFCMTokenInteractor(localization, userCollection), markMentionSeen: new MarkMentionSeenInteractor(localization, userCollection), diff --git a/packages/api/src/components/orgAuthStrategies.ts b/packages/api/src/components/orgAuthStrategies.ts index 73ec4e800..6b50d4e77 100644 --- a/packages/api/src/components/orgAuthStrategies.ts +++ b/packages/api/src/components/orgAuthStrategies.ts @@ -200,7 +200,7 @@ export class UserWithinOrgStrategy if (user) { const userOrgs = new Set(user.roles.map((r) => r.org_id) ?? empty) for (const orgId of userOrgs) { - // only admins can take actions on user entities in their org + // only admins can take actions on user entities in their org (e.g. resetPassword) if (this.authenticator.isAuthorized(requestCtx.identity, orgId, RoleType.Admin)) { return true } diff --git a/packages/api/src/interactors/CreateNewUserInteractor.ts b/packages/api/src/interactors/CreateNewUserInteractor.ts index 72554b104..318b8fe64 100644 --- a/packages/api/src/interactors/CreateNewUserInteractor.ts +++ b/packages/api/src/interactors/CreateNewUserInteractor.ts @@ -24,9 +24,7 @@ export class CreateNewUserInteractor ) {} public async execute({ user }: MutationCreateNewUserArgs): Promise { - const checkUser = await this.users.count({ - email: user.email - }) + const checkUser = await this.users.count({ email: user.email }) if (checkUser !== 0) { return new FailedResponse(this.localization.t('mutation.createNewUser.emailExist')) diff --git a/packages/api/src/interactors/DeleteUserInteractor.ts b/packages/api/src/interactors/RemoveUserFromOrganizationInteractor.ts similarity index 62% rename from packages/api/src/interactors/DeleteUserInteractor.ts rename to packages/api/src/interactors/RemoveUserFromOrganizationInteractor.ts index 93c6d9fc3..7d887736b 100644 --- a/packages/api/src/interactors/DeleteUserInteractor.ts +++ b/packages/api/src/interactors/RemoveUserFromOrganizationInteractor.ts @@ -2,13 +2,18 @@ * Copyright (c) Microsoft. All rights reserved. * Licensed under the MIT license. See LICENSE file in the project. */ -import { MutationDeleteUserArgs, VoidResponse } from '@cbosuite/schema/dist/provider-types' +import { + MutationRemoveUserFromOrganizationArgs, + VoidResponse +} from '@cbosuite/schema/dist/provider-types' import { Localization } from '~components' import { EngagementCollection, UserCollection } from '~db' import { Interactor, RequestContext } from '~types' import { FailedResponse, SuccessVoidResponse } from '~utils/response' -export class DeleteUserInteractor implements Interactor { +export class RemoveUserFromOrganizationInteractor + implements Interactor +{ public constructor( private readonly localization: Localization, private readonly users: UserCollection, @@ -16,53 +21,50 @@ export class DeleteUserInteractor implements Interactor { - // Delete user - try { - await this.users.deleteItem({ id: userId }) - } catch (error) { + const { item: user } = await this.users.itemById(userId) + if (!user) { return new FailedResponse(this.localization.t('mutation.deleteUser.fail')) } - // Remove all engagements with user + // Remove org permissinos + const newRoles = user.roles.filter((r) => r.org_id !== orgId) try { + if (newRoles.length === 0) { + await this.users.deleteItem({ id: userId }) + } else { + await this.users.updateItem({ id: userId }, { $set: { roles: newRoles } }) + } + + // Remove all engagements with user await this.engagements.deleteItems({ user_id: userId }) - } catch (error) { - return new FailedResponse(this.localization.t('mutation.deleteUser.fail')) - } - // Remove all remaining engagement actions with user - try { - const remainingEngagementsOnOrg = await this.engagements.items( - {}, - { org_id: identity?.roles[0]?.org_id } - ) - if (remainingEngagementsOnOrg.items) { - for (const engagement of remainingEngagementsOnOrg.items) { + // Remove all remaining engagement actions with user + const { items: remainingEngagements } = await this.engagements.items({}, { org_id: orgId }) + if (remainingEngagements) { + for (const engagement of remainingEngagements) { const newActions = [] - let removedUser = false + let isUpdated = false for (const action of engagement.actions) { // If action was created by user, do not added it to the newActions list if (action.user_id === userId) { - removedUser = true + isUpdated = true continue } - // If action tags the user, remove the tag if (action.tagged_user_id === userId) { action.tagged_user_id = undefined - removedUser = true + isUpdated = true } - // Add the action back to the engagment actions newActions.push(action) } // Only update the engagement actions if a user was removed - if (removedUser) { + if (isUpdated) { await this.engagements.updateItem( { id: engagement.id }, { @@ -78,6 +80,16 @@ export class DeleteUserInteractor implements Interactor & IResolvers = { updateUser: async (_, args, { requestCtx, interactors: { updateUser } }) => updateUser.execute(args, requestCtx), - deleteUser: async (_, args, { requestCtx, interactors: { deleteUser } }) => - deleteUser.execute(args, requestCtx), + removeUserFromOrganization: async ( + _, + args, + { requestCtx, interactors: { removeUserFromOrganization } } + ) => removeUserFromOrganization.execute(args, requestCtx), updateUserFCMToken: async (_, args, { requestCtx, interactors: { updateUserFCMToken } }) => updateUserFCMToken.execute(args, requestCtx), diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index b295c7a88..39b658954 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -23,7 +23,7 @@ import { QueryServicesArgs, QueryUserArgs, MutationResetUserPasswordArgs, - MutationDeleteUserArgs, + MutationRemoveUserFromOrganizationArgs, MutationArchiveContactArgs, QueryContactArgs, QueryContactsArgs, @@ -139,7 +139,7 @@ export interface BuiltAppContext { resetUserPassword: Interactor setUserPassword: Interactor createNewUser: Interactor - deleteUser: Interactor + removeUserFromOrganization: Interactor updateUser: Interactor updateUserFCMToken: Interactor markMentionSeen: Interactor diff --git a/packages/schema/schema.gql b/packages/schema/schema.gql index e3d4132c0..a3167d1ac 100644 --- a/packages/schema/schema.gql +++ b/packages/schema/schema.gql @@ -113,7 +113,8 @@ type Mutation { # # Delete user # - deleteUser(userId: String!): VoidResponse @orgAuth(requires: ADMIN) + removeUserFromOrganization(userId: String!, orgId: String!): VoidResponse + @orgAuth(requires: ADMIN) # # Update user FCM Token diff --git a/packages/webapp/src/components/forms/EditSpecialistForm/index.tsx b/packages/webapp/src/components/forms/EditSpecialistForm/index.tsx index 1caf15853..04b3e7902 100644 --- a/packages/webapp/src/components/forms/EditSpecialistForm/index.tsx +++ b/packages/webapp/src/components/forms/EditSpecialistForm/index.tsx @@ -110,7 +110,7 @@ export const EditSpecialistForm: StandardFC = wrap( } const handleDeleteSpecialist = async (sid: string) => { - await deleteSpecialist(sid) + await deleteSpecialist(orgId, sid) setShowModal(false) closeForm() } diff --git a/packages/webapp/src/hooks/api/useSpecialist/useDeleteSpecialistCallback.ts b/packages/webapp/src/hooks/api/useSpecialist/useDeleteSpecialistCallback.ts index 0f0aedd72..e83fdf6af 100644 --- a/packages/webapp/src/hooks/api/useSpecialist/useDeleteSpecialistCallback.ts +++ b/packages/webapp/src/hooks/api/useSpecialist/useDeleteSpecialistCallback.ts @@ -7,7 +7,7 @@ import { VoidResponse, Organization, StatusType, - MutationDeleteUserArgs + MutationRemoveUserFromOrganizationArgs } from '@cbosuite/schema/dist/client-types' import { MessageResponse } from '../types' import { useToasts } from '~hooks/useToasts' @@ -17,29 +17,29 @@ import { organizationState } from '~store' import { useCallback } from 'react' const DELETE_SPECIALIST = gql` - mutation deleteUser($userId: String!) { - deleteUser(userId: $userId) { + mutation removeUserFromOrganization($orgId: String!, $userId: String!) { + removeUserFromOrganization(orgId: $orgId, userId: $userId) { message status } } ` -export type DeleteSpecialistCallback = (userId: string) => Promise +export type DeleteSpecialistCallback = (orgId: string, userId: string) => Promise export function useDeleteSpecialistCallback(): DeleteSpecialistCallback { const { c } = useTranslation() const { success, failure } = useToasts() - const [deleteUser] = useMutation(DELETE_SPECIALIST) + const [deleteUser] = useMutation(DELETE_SPECIALIST) const [organization, setOrg] = useRecoilState(organizationState) return useCallback( - async (userId) => { + async (orgId: string, userId: string) => { const result: MessageResponse = { status: StatusType.Failed } try { await deleteUser({ - variables: { userId }, + variables: { userId, orgId }, update(cache, { data }) { const updateUserResp = data.deleteUser as VoidResponse