From fe5170573010b4de6a0329b60aa88281c969775e Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 4 Jan 2025 19:13:33 -0600 Subject: [PATCH] Add endpoint to disable user invitation --- libs/common/src/constants/controller-route.ts | 6 + .../controllers/invite-user.controller.ts | 23 ++- .../dtos/update.invite-user.dto.ts | 22 +++ .../services/invite-user.service.ts | 145 +++++++++++++++++- 4 files changed, 190 insertions(+), 6 deletions(-) diff --git a/libs/common/src/constants/controller-route.ts b/libs/common/src/constants/controller-route.ts index 934414d..2e627e9 100644 --- a/libs/common/src/constants/controller-route.ts +++ b/libs/common/src/constants/controller-route.ts @@ -766,6 +766,12 @@ export class ControllerRoute { public static readonly UPDATE_USER_INVITATION_DESCRIPTION = 'This endpoint updates an invitation for a user to assign to role and spaces.'; + public static readonly DISABLE_USER_INVITATION_SUMMARY = + 'Disable user invitation'; + + public static readonly DISABLE_USER_INVITATION_DESCRIPTION = + 'This endpoint disables an invitation for a user to assign to role and spaces.'; + public static readonly ACTIVATION_CODE_SUMMARY = 'Activate Invitation Code'; diff --git a/src/invite-user/controllers/invite-user.controller.ts b/src/invite-user/controllers/invite-user.controller.ts index 9da2224..f090bc5 100644 --- a/src/invite-user/controllers/invite-user.controller.ts +++ b/src/invite-user/controllers/invite-user.controller.ts @@ -17,7 +17,10 @@ import { Permissions } from 'src/decorators/permissions.decorator'; import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard'; import { CheckEmailDto } from '../dtos/check-email.dto'; import { ActivateCodeDto } from '../dtos/active-code.dto'; -import { UpdateUserInvitationDto } from '../dtos/update.invite-user.dto'; +import { + DisableUserInvitationDto, + UpdateUserInvitationDto, +} from '../dtos/update.invite-user.dto'; @ApiTags('Invite User Module') @Controller({ @@ -93,4 +96,22 @@ export class InviteUserController { invitedUserUuid, ); } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Put(':invitedUserUuid/disable') + @ApiOperation({ + summary: + ControllerRoute.INVITE_USER.ACTIONS.DISABLE_USER_INVITATION_SUMMARY, + description: + ControllerRoute.INVITE_USER.ACTIONS.DISABLE_USER_INVITATION_DESCRIPTION, + }) + async disableUserInvitation( + @Param('invitedUserUuid') invitedUserUuid: string, + @Body() disableUserInvitationDto: DisableUserInvitationDto, + ): Promise { + return await this.inviteUserService.disableUserInvitation( + disableUserInvitationDto, + invitedUserUuid, + ); + } } diff --git a/src/invite-user/dtos/update.invite-user.dto.ts b/src/invite-user/dtos/update.invite-user.dto.ts index 9e80306..6890ed1 100644 --- a/src/invite-user/dtos/update.invite-user.dto.ts +++ b/src/invite-user/dtos/update.invite-user.dto.ts @@ -2,6 +2,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { ArrayMinSize, IsArray, + IsBoolean, IsNotEmpty, IsOptional, IsString, @@ -72,3 +73,24 @@ export class UpdateUserInvitationDto { Object.assign(this, dto); } } +export class DisableUserInvitationDto { + @ApiProperty({ + description: 'The disable status of the user', + example: 'true', + required: true, + }) + @IsBoolean() + @IsNotEmpty() + public disable: boolean; + @ApiProperty({ + description: 'The project uuid of the user', + example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', + required: true, + }) + @IsString() + @IsNotEmpty() + public projectUuid: string; + constructor(dto: Partial) { + Object.assign(this, dto); + } +} diff --git a/src/invite-user/services/invite-user.service.ts b/src/invite-user/services/invite-user.service.ts index fd41f96..c56f71c 100644 --- a/src/invite-user/services/invite-user.service.ts +++ b/src/invite-user/services/invite-user.service.ts @@ -24,7 +24,10 @@ import { SpaceEntity, SpaceRepository } from '@app/common/modules/space'; import { ActivateCodeDto } from '../dtos/active-code.dto'; import { UserSpaceService } from 'src/users/services'; import { SpaceUserService } from 'src/space/services'; -import { UpdateUserInvitationDto } from '../dtos/update.invite-user.dto'; +import { + DisableUserInvitationDto, + UpdateUserInvitationDto, +} from '../dtos/update.invite-user.dto'; @Injectable() export class InviteUserService { @@ -285,10 +288,7 @@ export class InviteUserService { } // Perform update actions if status is 'INVITED' - if ( - userOldData.status === UserStatusEnum.INVITED || - userOldData.status === UserStatusEnum.DISABLED - ) { + if (userOldData.status === UserStatusEnum.INVITED) { await this.updateWhenUserIsInviteOrDisable( queryRunner, dto, @@ -475,4 +475,139 @@ export class InviteUserService { } } } + async disableUserInvitation( + dto: DisableUserInvitationDto, + invitedUserUuid: string, + ): Promise { + const { disable, projectUuid } = dto; + + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.startTransaction(); + + try { + const userData = await this.inviteUserRepository.findOne({ + where: { uuid: invitedUserUuid, project: { uuid: projectUuid } }, + relations: ['roleType', 'spaces.space'], + }); + + if (!userData) { + throw new HttpException('User not found', HttpStatus.NOT_FOUND); + } + + if (userData.status === UserStatusEnum.INVITED) { + await this.updateUserStatus(invitedUserUuid, projectUuid, disable); + } else if (userData.status === UserStatusEnum.ACTIVE) { + const user = await this.userRepository.findOne({ + where: { inviteUser: { uuid: invitedUserUuid } }, + relations: ['userSpaces.space', 'userSpaces.space.community'], + }); + + if (!user) { + throw new HttpException( + 'User account not found', + HttpStatus.NOT_FOUND, + ); + } + + if (!disable) { + await this.disassociateUserFromSpaces(user, projectUuid); + await this.updateUserStatus(invitedUserUuid, projectUuid, disable); + } else if (disable) { + await this.associateUserToSpaces( + user, + userData, + projectUuid, + invitedUserUuid, + disable, + ); + } + } else { + throw new HttpException( + 'Invalid user status or action', + HttpStatus.BAD_REQUEST, + ); + } + + await queryRunner.commitTransaction(); + + return new SuccessResponseDto({ + statusCode: HttpStatus.OK, + success: true, + message: 'User invitation status updated successfully', + }); + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } + } + + private async updateUserStatus( + invitedUserUuid: string, + projectUuid: string, + disable: boolean, + ) { + await this.inviteUserRepository.update( + { uuid: invitedUserUuid, project: { uuid: projectUuid } }, + { isEnabled: disable }, + ); + } + + private async disassociateUserFromSpaces(user: any, projectUuid: string) { + const disassociatePromises = user.userSpaces.map((userSpace) => + this.spaceUserService.disassociateUserFromSpace({ + communityUuid: userSpace.space.community.uuid, + spaceUuid: userSpace.space.uuid, + userUuid: user.uuid, + projectUuid, + }), + ); + + const results = await Promise.allSettled(disassociatePromises); + + results.forEach((result, index) => { + if (result.status === 'rejected') { + console.error( + `Failed to disassociate user from space ${user.userSpaces[index].space.uuid}:`, + result.reason, + ); + } + }); + } + private async associateUserToSpaces( + user: any, + userData: any, + projectUuid: string, + invitedUserUuid: string, + disable: boolean, + ) { + const spaceUuids = userData.spaces.map((space) => space.space.uuid); + + const associatePromises = spaceUuids.map(async (spaceUuid) => { + try { + const spaceDetails = await this.getSpaceByUuid(spaceUuid); + + const deviceUUIDs = + await this.userSpaceService.getDeviceUUIDsForSpace(spaceUuid); + await this.userSpaceService.addUserPermissionsToDevices( + user.uuid, + deviceUUIDs, + ); + + await this.spaceUserService.associateUserToSpace({ + communityUuid: spaceDetails.communityUuid, + spaceUuid, + userUuid: user.uuid, + projectUuid, + }); + + await this.updateUserStatus(invitedUserUuid, projectUuid, disable); + } catch (error) { + console.error(`Failed to associate user to space ${spaceUuid}:`, error); + } + }); + + await Promise.allSettled(associatePromises); + } }