From 93d05162fa8b8e883321505c590032bd28a6ba0a Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 4 Jan 2025 04:19:19 -0600 Subject: [PATCH 1/8] Remove `isEnabled` column from `InviteUserEntity` --- .../src/modules/Invite-user/entities/Invite-user.entity.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/libs/common/src/modules/Invite-user/entities/Invite-user.entity.ts b/libs/common/src/modules/Invite-user/entities/Invite-user.entity.ts index 3eac7d6..491d16f 100644 --- a/libs/common/src/modules/Invite-user/entities/Invite-user.entity.ts +++ b/libs/common/src/modules/Invite-user/entities/Invite-user.entity.ts @@ -56,12 +56,6 @@ export class InviteUserEntity extends AbstractEntity { }) public phoneNumber: string; - @Column({ - nullable: false, - default: true, - }) - public isEnabled: boolean; - @Column({ nullable: false, default: true, From 9c03b9e95dbd533e9ad08c3358e85170f129c11c Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 4 Jan 2025 04:19:34 -0600 Subject: [PATCH 2/8] Remove unused UserRepository from project.module.ts --- src/project/project.module.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/project/project.module.ts b/src/project/project.module.ts index 92b12f2..0258881 100644 --- a/src/project/project.module.ts +++ b/src/project/project.module.ts @@ -7,7 +7,6 @@ import { CreateOrphanSpaceHandler } from './handler'; import { SpaceRepository } from '@app/common/modules/space'; import { CommunityRepository } from '@app/common/modules/community/repositories'; import { InviteUserRepository } from '@app/common/modules/Invite-user/repositiories'; -import { UserRepository } from '@app/common/modules/user/repositories'; import { ProjectUserController } from './controllers/project-user.controller'; import { ProjectUserService } from './services/project-user.service'; @@ -25,7 +24,6 @@ const CommandHandlers = [CreateOrphanSpaceHandler]; ProjectUserService, ProjectRepository, InviteUserRepository, - UserRepository, ], exports: [ProjectService, CqrsModule], }) From eac5fce859c966b363e4345f3123870f53b749ca Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 4 Jan 2025 04:19:48 -0600 Subject: [PATCH 3/8] Simplify user retrieval and normalization in ProjectUserService --- src/project/services/project-user.service.ts | 78 ++++---------------- 1 file changed, 14 insertions(+), 64 deletions(-) diff --git a/src/project/services/project-user.service.ts b/src/project/services/project-user.service.ts index f48ddfb..847774f 100644 --- a/src/project/services/project-user.service.ts +++ b/src/project/services/project-user.service.ts @@ -1,18 +1,13 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; - -import { UserStatusEnum } from '@app/common/constants/user-status.enum'; -import { RoleType } from '@app/common/constants/role.type.enum'; import { InviteUserRepository } from '@app/common/modules/Invite-user/repositiories'; -import { UserRepository } from '@app/common/modules/user/repositories'; import { ProjectService } from './project.service'; @Injectable() export class ProjectUserService { constructor( private readonly inviteUserRepository: InviteUserRepository, - private readonly userRepository: UserRepository, private readonly projectService: ProjectService, ) {} @@ -25,8 +20,7 @@ export class ProjectUserService { HttpStatus.NOT_FOUND, ); } - // Fetch invited users - const invitedUsers = await this.inviteUserRepository.find({ + const allUsers = await this.inviteUserRepository.find({ where: { project: { uuid }, isActive: true }, select: [ 'uuid', @@ -38,35 +32,20 @@ export class ProjectUserService { 'phoneNumber', 'jobTitle', 'invitedBy', - 'isEnabled', ], relations: ['roleType'], }); - // Fetch project users - const users = await this.userRepository.find({ - where: { project: { uuid }, isActive: true }, - select: ['uuid', 'firstName', 'lastName', 'email', 'createdAt'], - relations: ['roleType'], - }); - - // Combine both arrays - const allUsers = [...users, ...invitedUsers]; - const normalizedUsers = allUsers.map((user) => { const createdAt = new Date(user.createdAt); const createdDate = createdAt.toLocaleDateString(); const createdTime = createdAt.toLocaleTimeString(); - // Normalize user properties - const normalizedProps = this.normalizeUserProperties(user); - - // Return the normalized user object return { ...user, + roleType: user.roleType.type, createdDate, createdTime, - ...normalizedProps, }; }); @@ -91,8 +70,7 @@ export class ProjectUserService { userUuid: string, ): Promise { try { - let user; - user = await this.inviteUserRepository.findOne({ + const user = await this.inviteUserRepository.findOne({ where: { project: { uuid: projectUuid }, uuid: userUuid, @@ -108,31 +86,28 @@ export class ProjectUserService { 'phoneNumber', 'jobTitle', 'invitedBy', - 'isEnabled', ], relations: ['roleType', 'spaces.space'], }); - if (!user) { - user = await this.userRepository.findOne({ - where: { - project: { uuid: projectUuid }, - uuid: userUuid, - isActive: true, - }, - select: ['uuid', 'firstName', 'lastName', 'email', 'createdAt'], - relations: ['roleType', 'userSpaces.space'], - }); - } + if (!user) { throw new HttpException( `User with ID ${userUuid} not found in project ${projectUuid}`, HttpStatus.NOT_FOUND, ); } - const responseData = this.formatUserResponse(user); + const createdAt = new Date(user.createdAt); + const createdDate = createdAt.toLocaleDateString(); + const createdTime = createdAt.toLocaleTimeString(); return new SuccessResponseDto({ message: `User in project with ID ${projectUuid} retrieved successfully`, - data: responseData, + data: { + ...user, + roleType: user.roleType.type, + createdDate, + createdTime, + spaces: user.spaces.map((space) => space.space), + }, statusCode: HttpStatus.OK, }); } catch (error) { @@ -146,29 +121,4 @@ export class ProjectUserService { ); } } - private formatUserResponse(user: any) { - const createdAt = new Date(user.createdAt); - return { - uuid: user.uuid, - firstName: user.firstName, - lastName: user.lastName, - email: user.email, - createdDate: createdAt.toLocaleDateString(), - createdTime: createdAt.toLocaleTimeString(), - ...this.normalizeUserProperties(user), - }; - } - private normalizeUserProperties(user: any) { - return { - status: user.status ?? UserStatusEnum.ACTIVE, - invitedBy: user.invitedBy ?? RoleType.SPACE_MEMBER, - isEnabled: user.isEnabled ?? true, - phoneNumber: user.phoneNumber ?? null, - jobTitle: user.jobTitle ?? null, - roleType: user.roleType?.type ?? null, - spaces: - user?.spaces?.map((space: any) => space?.space) ?? - user?.userSpaces?.map((space: any) => space?.space), - }; - } } From 77061d3ce3aa89bcf1120b2a3dd37aa4cbd354a3 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 4 Jan 2025 04:26:37 -0600 Subject: [PATCH 4/8] Rename userUuid to invitedUserUuid in project-user controller and service --- src/project/controllers/project-user.controller.ts | 6 +++--- src/project/services/project-user.service.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/project/controllers/project-user.controller.ts b/src/project/controllers/project-user.controller.ts index d7f5e31..7cda75e 100644 --- a/src/project/controllers/project-user.controller.ts +++ b/src/project/controllers/project-user.controller.ts @@ -35,14 +35,14 @@ export class ProjectUserController { description: ControllerRoute.PROJECT.ACTIONS.GET_USERS_BY_PROJECT_DESCRIPTION, }) - @Get(':userUuid') + @Get(':invitedUserUuid') async findUserByUuidInProject( @Param() params: GetProjectParam, - @Param('userUuid') userUuid: string, + @Param('invitedUserUuid') invitedUserUuid: string, ): Promise { return this.projectUserService.getUserByUuidInProject( params.projectUuid, - userUuid, + invitedUserUuid, ); } } diff --git a/src/project/services/project-user.service.ts b/src/project/services/project-user.service.ts index 847774f..53d64f1 100644 --- a/src/project/services/project-user.service.ts +++ b/src/project/services/project-user.service.ts @@ -67,13 +67,13 @@ export class ProjectUserService { } async getUserByUuidInProject( projectUuid: string, - userUuid: string, + invitedUserUuid: string, ): Promise { try { const user = await this.inviteUserRepository.findOne({ where: { project: { uuid: projectUuid }, - uuid: userUuid, + uuid: invitedUserUuid, isActive: true, }, select: [ @@ -92,7 +92,7 @@ export class ProjectUserService { if (!user) { throw new HttpException( - `User with ID ${userUuid} not found in project ${projectUuid}`, + `User with ID ${invitedUserUuid} not found in project ${projectUuid}`, HttpStatus.NOT_FOUND, ); } From 33c9518ca541375b95c9c3cbe73792bf452160bf Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 4 Jan 2025 04:34:11 -0600 Subject: [PATCH 5/8] Include `devices.productDevice` in space validation --- src/space/services/space-validation.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/space/services/space-validation.service.ts b/src/space/services/space-validation.service.ts index 89a539a..d9a9f04 100644 --- a/src/space/services/space-validation.service.ts +++ b/src/space/services/space-validation.service.ts @@ -51,6 +51,7 @@ export class ValidationService { 'subspaces.tags', 'subspaces.devices', 'devices', + 'devices.productDevice', ], }); From c616547f9b41e8b05aed9937bfed6019656c77c1 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 4 Jan 2025 04:44:34 -0600 Subject: [PATCH 6/8] Add activation code endpoint --- libs/common/src/constants/controller-route.ts | 6 ++ .../controllers/invite-user.controller.ts | 16 ++++ src/invite-user/dtos/active-code.dto.ts | 22 +++++ src/invite-user/invite-user.module.ts | 29 +++++- .../services/invite-user.service.ts | 88 +++++++++++++++++++ src/users/services/user-space.service.ts | 6 +- 6 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 src/invite-user/dtos/active-code.dto.ts diff --git a/libs/common/src/constants/controller-route.ts b/libs/common/src/constants/controller-route.ts index 1d23d9f..17222b1 100644 --- a/libs/common/src/constants/controller-route.ts +++ b/libs/common/src/constants/controller-route.ts @@ -760,6 +760,12 @@ export class ControllerRoute { public static readonly CREATE_USER_INVITATION_DESCRIPTION = 'This endpoint creates an invitation for a user to assign to role and spaces.'; + public static readonly ACTIVATION_CODE_SUMMARY = + 'Activate Invitation Code'; + + public static readonly ACTIVATION_CODE_DESCRIPTION = + 'This endpoint activate invitation code'; + public static readonly CHECK_EMAIL_SUMMARY = 'Check email'; public static readonly CHECK_EMAIL_DESCRIPTION = diff --git a/src/invite-user/controllers/invite-user.controller.ts b/src/invite-user/controllers/invite-user.controller.ts index 4351345..4bf493f 100644 --- a/src/invite-user/controllers/invite-user.controller.ts +++ b/src/invite-user/controllers/invite-user.controller.ts @@ -8,6 +8,7 @@ import { PermissionsGuard } from 'src/guards/permissions.guard'; 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'; @ApiTags('Invite User Module') @Controller({ @@ -51,4 +52,19 @@ export class InviteUserController { addUserInvitationDto, ); } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post('activation') + @ApiOperation({ + summary: ControllerRoute.INVITE_USER.ACTIONS.ACTIVATION_CODE_SUMMARY, + description: + ControllerRoute.INVITE_USER.ACTIONS.ACTIVATION_CODE_DESCRIPTION, + }) + async activationCodeController( + @Body() activateCodeDto: ActivateCodeDto, + ): Promise { + return await this.inviteUserService.activationCodeController( + activateCodeDto, + ); + } } diff --git a/src/invite-user/dtos/active-code.dto.ts b/src/invite-user/dtos/active-code.dto.ts new file mode 100644 index 0000000..ea852aa --- /dev/null +++ b/src/invite-user/dtos/active-code.dto.ts @@ -0,0 +1,22 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsNotEmpty } from 'class-validator'; + +export class ActivateCodeDto { + @ApiProperty({ + description: 'The activation code of the user', + example: '7CvRcA', + required: true, + }) + @IsString() + @IsNotEmpty() + activationCode: string; + + @ApiProperty({ + description: 'The UUID of the user', + example: 'd7a44e8a-32d5-4f39-ae2e-013f1245aead', + required: true, + }) + @IsString() + @IsNotEmpty() + userUuid: string; +} diff --git a/src/invite-user/invite-user.module.ts b/src/invite-user/invite-user.module.ts index 402ebb1..3a8e20e 100644 --- a/src/invite-user/invite-user.module.ts +++ b/src/invite-user/invite-user.module.ts @@ -3,13 +3,27 @@ import { InviteUserService } from './services/invite-user.service'; import { InviteUserController } from './controllers/invite-user.controller'; import { ConfigModule } from '@nestjs/config'; -import { UserRepository } from '@app/common/modules/user/repositories'; +import { + UserRepository, + UserSpaceRepository, +} from '@app/common/modules/user/repositories'; import { InviteUserRepositoryModule } from '@app/common/modules/Invite-user/Invite-user.repository.module'; import { InviteUserRepository, InviteUserSpaceRepository, } from '@app/common/modules/Invite-user/repositiories'; import { EmailService } from '@app/common/util/email.service'; +import { SpaceUserService, ValidationService } from 'src/space/services'; +import { CommunityService } from 'src/community/services'; +import { SpaceRepository } from '@app/common/modules/space'; +import { SpaceModelRepository } from '@app/common/modules/space-model'; +import { CommunityRepository } from '@app/common/modules/community/repositories'; +import { ProjectRepository } from '@app/common/modules/project/repositiories'; +import { TuyaService } from '@app/common/integrations/tuya/services/tuya.service'; +import { UserSpaceService } from 'src/users/services'; +import { UserDevicePermissionService } from 'src/user-device-permission/services'; +import { DeviceUserPermissionRepository } from '@app/common/modules/device/repositories'; +import { PermissionTypeRepository } from '@app/common/modules/permission/repositories'; @Module({ imports: [ConfigModule, InviteUserRepositoryModule], @@ -20,6 +34,19 @@ import { EmailService } from '@app/common/util/email.service'; UserRepository, EmailService, InviteUserSpaceRepository, + SpaceUserService, + ValidationService, + UserSpaceRepository, + CommunityService, + SpaceRepository, + SpaceModelRepository, + CommunityRepository, + ProjectRepository, + TuyaService, + UserSpaceService, + UserDevicePermissionService, + DeviceUserPermissionRepository, + PermissionTypeRepository, ], exports: [InviteUserService], }) diff --git a/src/invite-user/services/invite-user.service.ts b/src/invite-user/services/invite-user.service.ts index aaeabd7..2376b3d 100644 --- a/src/invite-user/services/invite-user.service.ts +++ b/src/invite-user/services/invite-user.service.ts @@ -16,6 +16,9 @@ import { CheckEmailDto } from '../dtos/check-email.dto'; import { UserRepository } from '@app/common/modules/user/repositories'; import { EmailService } from '@app/common/util/email.service'; import { SpaceEntity } from '@app/common/modules/space'; +import { ActivateCodeDto } from '../dtos/active-code.dto'; +import { UserSpaceService } from 'src/users/services'; +import { SpaceUserService } from 'src/space/services'; @Injectable() export class InviteUserService { @@ -24,6 +27,9 @@ export class InviteUserService { private readonly userRepository: UserRepository, private readonly emailService: EmailService, private readonly inviteUserSpaceRepository: InviteUserSpaceRepository, + private readonly userSpaceService: UserSpaceService, + private readonly spaceUserService: SpaceUserService, + private readonly dataSource: DataSource, ) {} @@ -171,4 +177,86 @@ export class InviteUserService { ); } } + async activationCodeController( + dto: ActivateCodeDto, + ): Promise { + try { + const { activationCode, userUuid } = dto; + const user = await this.userRepository.findOne({ + where: { uuid: userUuid, isActive: true, isUserVerified: true }, + }); + + if (!user) { + throw new HttpException('User not found', HttpStatus.NOT_FOUND); + } + const { email } = user; + const invitedUser = await this.inviteUserRepository.findOne({ + where: { + email, + invitationCode: activationCode, + status: UserStatusEnum.INVITED, + isActive: true, + }, + relations: ['project', 'spaces.space.community'], + }); + + if (!invitedUser) { + throw new HttpException( + 'Invalid activation code', + HttpStatus.BAD_REQUEST, + ); + } + + for (const invitedSpace of invitedUser.spaces) { + try { + const deviceUUIDs = + await this.userSpaceService.getDeviceUUIDsForSpace( + invitedSpace.space.uuid, + ); + + await this.userSpaceService.addUserPermissionsToDevices( + userUuid, + deviceUUIDs, + ); + + await this.spaceUserService.associateUserToSpace({ + communityUuid: invitedSpace.space.community.uuid, + spaceUuid: invitedSpace.space.uuid, + userUuid: user.uuid, + projectUuid: invitedUser.project.uuid, + }); + } catch (spaceError) { + console.error( + `Error processing space ${invitedSpace.space.uuid}:`, + spaceError, + ); + // Skip to the next space + continue; + } + } + await this.inviteUserRepository.update( + { uuid: invitedUser.uuid }, + { status: UserStatusEnum.ACTIVE }, + ); + await this.userRepository.update( + { uuid: userUuid }, + { + project: { uuid: invitedUser.project.uuid }, + inviteUser: { uuid: invitedUser.uuid }, + }, + ); + return new SuccessResponseDto({ + statusCode: HttpStatus.OK, + success: true, + message: 'The code has been successfully activated', + }); + } catch (error) { + console.error('Error activating the code:', error); + throw new HttpException( + error.message || + 'An unexpected error occurred while activating the code', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } diff --git a/src/users/services/user-space.service.ts b/src/users/services/user-space.service.ts index 0ee9af5..0ee6b03 100644 --- a/src/users/services/user-space.service.ts +++ b/src/users/services/user-space.service.ts @@ -117,9 +117,7 @@ export class UserSpaceService { ); } - private async getDeviceUUIDsForSpace( - unitUuid: string, - ): Promise<{ uuid: string }[]> { + async getDeviceUUIDsForSpace(unitUuid: string): Promise<{ uuid: string }[]> { const devices = await this.spaceRepository.find({ where: { uuid: unitUuid }, relations: ['devices', 'devices.productDevice'], @@ -130,7 +128,7 @@ export class UserSpaceService { return allDevices.map((device) => ({ uuid: device.uuid })); } - private async addUserPermissionsToDevices( + async addUserPermissionsToDevices( userUuid: string, deviceUUIDs: { uuid: string }[], ): Promise { From 687d88ea2ead33b31d4010c3d5867fec4ef7f649 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 4 Jan 2025 06:21:39 -0600 Subject: [PATCH 7/8] Update user spaces retrieval based on user status --- src/project/services/project-user.service.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/project/services/project-user.service.ts b/src/project/services/project-user.service.ts index 53d64f1..d69332f 100644 --- a/src/project/services/project-user.service.ts +++ b/src/project/services/project-user.service.ts @@ -3,12 +3,15 @@ import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; import { InviteUserRepository } from '@app/common/modules/Invite-user/repositiories'; import { ProjectService } from './project.service'; +import { UserStatusEnum } from '@app/common/constants/user-status.enum'; +import { UserSpaceRepository } from '@app/common/modules/user/repositories'; @Injectable() export class ProjectUserService { constructor( private readonly inviteUserRepository: InviteUserRepository, private readonly projectService: ProjectService, + private readonly userSpaceRepository: UserSpaceRepository, ) {} async getUsersByProject(uuid: string): Promise { @@ -70,6 +73,7 @@ export class ProjectUserService { invitedUserUuid: string, ): Promise { try { + let userSpaces; const user = await this.inviteUserRepository.findOne({ where: { project: { uuid: projectUuid }, @@ -96,6 +100,15 @@ export class ProjectUserService { HttpStatus.NOT_FOUND, ); } + if (user.status === UserStatusEnum.ACTIVE) { + const spaces = await this.userSpaceRepository.find({ + where: { user: { inviteUser: { uuid: invitedUserUuid } } }, + relations: ['space'], + }); + userSpaces = spaces.map((space) => space.space); + } else { + userSpaces = user.spaces.map((space) => space.space); + } const createdAt = new Date(user.createdAt); const createdDate = createdAt.toLocaleDateString(); const createdTime = createdAt.toLocaleTimeString(); @@ -106,7 +119,7 @@ export class ProjectUserService { roleType: user.roleType.type, createdDate, createdTime, - spaces: user.spaces.map((space) => space.space), + spaces: userSpaces, }, statusCode: HttpStatus.OK, }); From 39d73bf241ad18cc2c60d0fd0e45dce549a12bbc Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 4 Jan 2025 06:25:50 -0600 Subject: [PATCH 8/8] finished Update invited user endpoint --- libs/common/src/constants/controller-route.ts | 6 + .../controllers/invite-user.controller.ts | 28 ++- .../dtos/update.invite-user.dto.ts | 74 ++++++ src/invite-user/invite-user.module.ts | 2 + .../services/invite-user.service.ts | 224 +++++++++++++++++- src/project/project.module.ts | 2 + 6 files changed, 331 insertions(+), 5 deletions(-) create mode 100644 src/invite-user/dtos/update.invite-user.dto.ts diff --git a/libs/common/src/constants/controller-route.ts b/libs/common/src/constants/controller-route.ts index 17222b1..934414d 100644 --- a/libs/common/src/constants/controller-route.ts +++ b/libs/common/src/constants/controller-route.ts @@ -760,6 +760,12 @@ export class ControllerRoute { public static readonly CREATE_USER_INVITATION_DESCRIPTION = 'This endpoint creates an invitation for a user to assign to role and spaces.'; + public static readonly UPDATE_USER_INVITATION_SUMMARY = + 'Update user invitation'; + + public static readonly UPDATE_USER_INVITATION_DESCRIPTION = + 'This endpoint updates 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 4bf493f..9da2224 100644 --- a/src/invite-user/controllers/invite-user.controller.ts +++ b/src/invite-user/controllers/invite-user.controller.ts @@ -1,5 +1,13 @@ import { InviteUserService } from '../services/invite-user.service'; -import { Body, Controller, Post, Req, UseGuards } from '@nestjs/common'; +import { + Body, + Controller, + Param, + Post, + Put, + Req, + UseGuards, +} from '@nestjs/common'; import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; import { AddUserInvitationDto } from '../dtos/add.invite-user.dto'; import { ControllerRoute } from '@app/common/constants/controller-route'; @@ -9,6 +17,7 @@ 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'; @ApiTags('Invite User Module') @Controller({ @@ -67,4 +76,21 @@ export class InviteUserController { activateCodeDto, ); } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Put(':invitedUserUuid') + @ApiOperation({ + summary: ControllerRoute.INVITE_USER.ACTIONS.UPDATE_USER_INVITATION_SUMMARY, + description: + ControllerRoute.INVITE_USER.ACTIONS.UPDATE_USER_INVITATION_DESCRIPTION, + }) + async updateUserInvitation( + @Param('invitedUserUuid') invitedUserUuid: string, + @Body() updateUserInvitationDto: UpdateUserInvitationDto, + ): Promise { + return await this.inviteUserService.updateUserInvitation( + updateUserInvitationDto, + invitedUserUuid, + ); + } } diff --git a/src/invite-user/dtos/update.invite-user.dto.ts b/src/invite-user/dtos/update.invite-user.dto.ts new file mode 100644 index 0000000..9e80306 --- /dev/null +++ b/src/invite-user/dtos/update.invite-user.dto.ts @@ -0,0 +1,74 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + ArrayMinSize, + IsArray, + IsNotEmpty, + IsOptional, + IsString, +} from 'class-validator'; + +export class UpdateUserInvitationDto { + @ApiProperty({ + description: 'The first name of the user', + example: 'John', + required: true, + }) + @IsString() + @IsNotEmpty() + public firstName: string; + + @ApiProperty({ + description: 'The last name of the user', + example: 'Doe', + required: true, + }) + @IsString() + @IsNotEmpty() + public lastName: string; + + @ApiProperty({ + description: 'The job title of the user', + example: 'Software Engineer', + required: false, + }) + @IsString() + @IsOptional() + public jobTitle?: string; + + @ApiProperty({ + description: 'The phone number of the user', + example: '+1234567890', + required: false, + }) + @IsString() + @IsOptional() + public phoneNumber?: string; + + @ApiProperty({ + description: 'The role uuid of the user', + example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', + required: true, + }) + @IsString() + @IsNotEmpty() + public roleUuid: string; + @ApiProperty({ + description: 'The project uuid of the user', + example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', + required: true, + }) + @IsString() + @IsNotEmpty() + public projectUuid: string; + @ApiProperty({ + description: 'The array of space UUIDs (at least one required)', + example: ['b5f3c9d2-58b7-4377-b3f7-60acb711d5d9'], + required: true, + }) + @IsArray() + @ArrayMinSize(1) + public spaceUuids: string[]; + constructor(dto: Partial) { + Object.assign(this, dto); + } +} diff --git a/src/invite-user/invite-user.module.ts b/src/invite-user/invite-user.module.ts index 3a8e20e..443b928 100644 --- a/src/invite-user/invite-user.module.ts +++ b/src/invite-user/invite-user.module.ts @@ -24,6 +24,7 @@ import { UserSpaceService } from 'src/users/services'; import { UserDevicePermissionService } from 'src/user-device-permission/services'; import { DeviceUserPermissionRepository } from '@app/common/modules/device/repositories'; import { PermissionTypeRepository } from '@app/common/modules/permission/repositories'; +import { ProjectUserService } from 'src/project/services/project-user.service'; @Module({ imports: [ConfigModule, InviteUserRepositoryModule], @@ -47,6 +48,7 @@ import { PermissionTypeRepository } from '@app/common/modules/permission/reposit UserDevicePermissionService, DeviceUserPermissionRepository, PermissionTypeRepository, + ProjectUserService, ], exports: [InviteUserService], }) diff --git a/src/invite-user/services/invite-user.service.ts b/src/invite-user/services/invite-user.service.ts index 2376b3d..fd41f96 100644 --- a/src/invite-user/services/invite-user.service.ts +++ b/src/invite-user/services/invite-user.service.ts @@ -1,10 +1,15 @@ -import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; +import { + Injectable, + HttpException, + HttpStatus, + BadRequestException, +} from '@nestjs/common'; import { AddUserInvitationDto } from '../dtos'; import { BaseResponseDto } from '@app/common/dto/base.response.dto'; import { UserStatusEnum } from '@app/common/constants/user-status.enum'; import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; import { generateRandomString } from '@app/common/helper/randomString'; -import { In, IsNull, Not } from 'typeorm'; +import { In, IsNull, Not, QueryRunner } from 'typeorm'; import { DataSource } from 'typeorm'; import { UserEntity } from '@app/common/modules/user/entities'; import { RoleType } from '@app/common/constants/role.type.enum'; @@ -15,10 +20,11 @@ import { import { CheckEmailDto } from '../dtos/check-email.dto'; import { UserRepository } from '@app/common/modules/user/repositories'; import { EmailService } from '@app/common/util/email.service'; -import { SpaceEntity } from '@app/common/modules/space'; +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'; @Injectable() export class InviteUserService { @@ -29,7 +35,7 @@ export class InviteUserService { private readonly inviteUserSpaceRepository: InviteUserSpaceRepository, private readonly userSpaceService: UserSpaceService, private readonly spaceUserService: SpaceUserService, - + private readonly spaceRepository: SpaceRepository, private readonly dataSource: DataSource, ) {} @@ -259,4 +265,214 @@ export class InviteUserService { ); } } + async updateUserInvitation( + dto: UpdateUserInvitationDto, + invitedUserUuid: string, + ): Promise { + const { projectUuid } = dto; + + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.startTransaction(); + + try { + // Fetch the user's existing data in the project + const userOldData = await this.inviteUserRepository.findOne({ + where: { uuid: invitedUserUuid, project: { uuid: projectUuid } }, + }); + + if (!userOldData) { + throw new HttpException('User not found', HttpStatus.NOT_FOUND); + } + + // Perform update actions if status is 'INVITED' + if ( + userOldData.status === UserStatusEnum.INVITED || + userOldData.status === UserStatusEnum.DISABLED + ) { + await this.updateWhenUserIsInviteOrDisable( + queryRunner, + dto, + invitedUserUuid, + ); + } else if (userOldData.status === UserStatusEnum.ACTIVE) { + await this.updateWhenUserIsActive(queryRunner, dto, invitedUserUuid); + } + + await queryRunner.commitTransaction(); + + return new SuccessResponseDto({ + statusCode: HttpStatus.OK, + success: true, + message: 'User invitation updated successfully', + }); + } catch (error) { + await queryRunner.rollbackTransaction(); + + // Throw an appropriate HTTP exception + throw error instanceof HttpException + ? error + : new HttpException( + 'An unexpected error occurred while updating the user', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + await queryRunner.release(); + } + } + + private async updateWhenUserIsInviteOrDisable( + queryRunner: QueryRunner, + dto: UpdateUserInvitationDto, + invitedUserUuid: string, + ): Promise { + const { firstName, lastName, jobTitle, phoneNumber, roleUuid, spaceUuids } = + dto; + + // Update user invitation details + await queryRunner.manager.update( + this.inviteUserRepository.target, + { uuid: invitedUserUuid }, + { + firstName, + lastName, + jobTitle, + phoneNumber, + roleType: { uuid: roleUuid }, + }, + ); + + // Remove old space associations + await queryRunner.manager.delete(this.inviteUserSpaceRepository.target, { + inviteUser: { uuid: invitedUserUuid }, + }); + + // Save new space associations + const spaceData = spaceUuids.map((spaceUuid) => ({ + inviteUser: { uuid: invitedUserUuid }, + space: { uuid: spaceUuid }, + })); + await queryRunner.manager.save( + this.inviteUserSpaceRepository.target, + spaceData, + ); + } + private async updateWhenUserIsActive( + queryRunner: QueryRunner, + dto: UpdateUserInvitationDto, + invitedUserUuid: string, + ): Promise { + const { + firstName, + lastName, + jobTitle, + phoneNumber, + roleUuid, + spaceUuids, + projectUuid, + } = dto; + + const user = await this.userRepository.findOne({ + where: { inviteUser: { uuid: invitedUserUuid } }, + relations: ['userSpaces.space', 'userSpaces.space.community'], + }); + + if (!user) { + throw new HttpException( + `User with invitedUserUuid ${invitedUserUuid} not found`, + HttpStatus.NOT_FOUND, + ); + } + + // Update user details + await queryRunner.manager.update( + this.inviteUserRepository.target, + { uuid: invitedUserUuid }, + { + firstName, + lastName, + jobTitle, + phoneNumber, + roleType: { uuid: roleUuid }, + }, + ); + + // Disassociate the user from all current spaces + const disassociatePromises = user.userSpaces.map((userSpace) => + this.spaceUserService + .disassociateUserFromSpace({ + communityUuid: userSpace.space.community.uuid, + spaceUuid: userSpace.space.uuid, + userUuid: user.uuid, + projectUuid, + }) + .catch((error) => { + console.error( + `Failed to disassociate user from space ${userSpace.space.uuid}:`, + error, + ); + throw error; + }), + ); + + await Promise.allSettled(disassociatePromises); + + // Process new spaces + const associatePromises = spaceUuids.map(async (spaceUuid) => { + try { + // Fetch space details + const spaceDetails = await this.getSpaceByUuid(spaceUuid); + + // Fetch device UUIDs for the space + const deviceUUIDs = + await this.userSpaceService.getDeviceUUIDsForSpace(spaceUuid); + + // Grant permissions to the user for all devices in the space + await this.userSpaceService.addUserPermissionsToDevices( + user.uuid, + deviceUUIDs, + ); + + // Associate the user with the new space + await this.spaceUserService.associateUserToSpace({ + communityUuid: spaceDetails.communityUuid, + spaceUuid: spaceUuid, + userUuid: user.uuid, + projectUuid, + }); + } catch (error) { + console.error(`Failed to process space ${spaceUuid}:`, error); + throw error; + } + }); + + await Promise.all(associatePromises); + } + + async getSpaceByUuid(spaceUuid: string) { + try { + const space = await this.spaceRepository.findOne({ + where: { + uuid: spaceUuid, + }, + relations: ['community'], + }); + if (!space) { + throw new BadRequestException(`Invalid space UUID ${spaceUuid}`); + } + return { + uuid: space.uuid, + createdAt: space.createdAt, + updatedAt: space.updatedAt, + name: space.spaceName, + spaceTuyaUuid: space.community.externalId, + communityUuid: space.community.uuid, + }; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; // Re-throw BadRequestException + } else { + throw new HttpException('Space not found', HttpStatus.NOT_FOUND); + } + } + } } diff --git a/src/project/project.module.ts b/src/project/project.module.ts index 0258881..260df68 100644 --- a/src/project/project.module.ts +++ b/src/project/project.module.ts @@ -9,6 +9,7 @@ import { CommunityRepository } from '@app/common/modules/community/repositories' import { InviteUserRepository } from '@app/common/modules/Invite-user/repositiories'; import { ProjectUserController } from './controllers/project-user.controller'; import { ProjectUserService } from './services/project-user.service'; +import { UserSpaceRepository } from '@app/common/modules/user/repositories'; const CommandHandlers = [CreateOrphanSpaceHandler]; @@ -24,6 +25,7 @@ const CommandHandlers = [CreateOrphanSpaceHandler]; ProjectUserService, ProjectRepository, InviteUserRepository, + UserSpaceRepository, ], exports: [ProjectService, CqrsModule], })