From 3cfed2b45215241af0eea6c42e23182b8c9457c1 Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Thu, 10 Jul 2025 10:56:27 +0300 Subject: [PATCH 1/6] fix: check if subspaces not exists in update space (#463) --- src/space/dtos/update.space.dto.ts | 5 +-- .../services/subspace/subspace.service.ts | 33 ++++++++++++------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/space/dtos/update.space.dto.ts b/src/space/dtos/update.space.dto.ts index 0491aa1..5f1fbcd 100644 --- a/src/space/dtos/update.space.dto.ts +++ b/src/space/dtos/update.space.dto.ts @@ -50,11 +50,12 @@ export class UpdateSpaceDto { description: 'List of subspace modifications', type: [UpdateSubspaceDto], }) - @ArrayUnique((subspace) => subspace.subspaceName, { + @ArrayUnique((subspace) => subspace.subspaceName ?? subspace.uuid, { message(validationArguments) { const subspaces = validationArguments.value; const nameCounts = subspaces.reduce((acc, curr) => { - acc[curr.subspaceName] = (acc[curr.subspaceName] || 0) + 1; + acc[curr.subspaceName ?? curr.uuid] = + (acc[curr.subspaceName ?? curr.uuid] || 0) + 1; return acc; }, {}); // Find duplicates diff --git a/src/space/services/subspace/subspace.service.ts b/src/space/services/subspace/subspace.service.ts index 15bda14..64d56c9 100644 --- a/src/space/services/subspace/subspace.service.ts +++ b/src/space/services/subspace/subspace.service.ts @@ -342,26 +342,37 @@ export class SubSpaceService { })), ); + const existingSubspaces = await this.subspaceRepository.find({ + where: { + uuid: In( + subspaceDtos.filter((dto) => dto.uuid).map((dto) => dto.uuid), + ), + }, + }); + + if ( + existingSubspaces.length !== + subspaceDtos.filter((dto) => dto.uuid).length + ) { + throw new HttpException( + `Some subspaces with provided UUIDs do not exist in the space.`, + HttpStatus.NOT_FOUND, + ); + } + const updatedSubspaces: SubspaceEntity[] = await queryRunner.manager.save( SubspaceEntity, - [ - ...newSubspaces, - ...subspaceDtos - .filter((dto) => dto.uuid) - .map((dto) => ({ - subspaceName: dto.subspaceName, - space, - })), - ], + newSubspaces, ); + const allSubspaces = [...updatedSubspaces, ...existingSubspaces]; // create or update allocations for the subspaces - if (updatedSubspaces.length > 0) { + if (allSubspaces.length > 0) { await this.subspaceProductAllocationService.updateSubspaceProductAllocationsV2( subspaceDtos.map((dto) => ({ ...dto, uuid: dto.uuid || - updatedSubspaces.find((s) => s.subspaceName === dto.subspaceName) + allSubspaces.find((s) => s.subspaceName === dto.subspaceName) ?.uuid, })), projectUuid, From 009deaf40328e9fdda475fb791f467aa1d78a241 Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Thu, 10 Jul 2025 11:11:30 +0300 Subject: [PATCH 2/6] SP-1812: fix: update bookable space (#467) --- src/booking/controllers/bookable-space.controller.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/booking/controllers/bookable-space.controller.ts b/src/booking/controllers/bookable-space.controller.ts index 65d7b33..876976b 100644 --- a/src/booking/controllers/bookable-space.controller.ts +++ b/src/booking/controllers/bookable-space.controller.ts @@ -7,6 +7,7 @@ import { Controller, Get, Param, + ParseUUIDPipe, Post, Put, Query, @@ -94,7 +95,7 @@ export class BookableSpaceController { .UPDATE_BOOKABLE_SPACES_DESCRIPTION, }) async update( - @Param('spaceUuid') spaceUuid: string, + @Param('spaceUuid', ParseUUIDPipe) spaceUuid: string, @Body() dto: UpdateBookableSpaceDto, ): Promise { const result = await this.bookableSpaceService.update(spaceUuid, dto); From 945328c0ce1269b039d11f587c502a469101ed93 Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Thu, 10 Jul 2025 11:33:55 +0300 Subject: [PATCH 3/6] fix: commission device API (#465) --- src/device/services/device.service.ts | 42 ++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/device/services/device.service.ts b/src/device/services/device.service.ts index 02210b7..9b3897c 100644 --- a/src/device/services/device.service.ts +++ b/src/device/services/device.service.ts @@ -323,7 +323,7 @@ export class DeviceService { async addNewDevice(addDeviceDto: AddDeviceDto, projectUuid: string) { try { - const device = await this.getDeviceDetailsByDeviceIdTuya( + const device = await this.getNewDeviceDetailsFromTuya( addDeviceDto.deviceTuyaUuid, ); @@ -349,6 +349,7 @@ export class DeviceService { spaceDevice: { uuid: addDeviceDto.spaceUuid }, tag: { uuid: addDeviceDto.tagUuid }, name: addDeviceDto.deviceName, + deviceTuyaConstUuid: device.uuid, }); if (deviceSaved.uuid) { const deviceStatus: BaseResponseDto = @@ -752,6 +753,45 @@ export class DeviceService { ); } } + + async getNewDeviceDetailsFromTuya( + deviceId: string, + ): Promise { + console.log('fetching device details from Tuya for deviceId:', deviceId); + try { + const result = await this.tuyaService.getDeviceDetails(deviceId); + + if (!result) { + throw new NotFoundException('Device not found'); + } + // Convert keys to camel case + const camelCaseResponse = convertKeysToCamelCase(result); + + const product = await this.productRepository.findOne({ + where: { prodId: camelCaseResponse.productId }, + }); + + if (!product) { + throw new NotFoundException('Product Type is not supported'); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { productId, id, productName, ...rest } = camelCaseResponse; + + return { + ...rest, + productUuid: product.uuid, + productName: product.name, + } as GetDeviceDetailsInterface; + } catch (error) { + console.log('error', error); + + throw new HttpException( + error.message || 'Error fetching device details from Tuya', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } async getDeviceInstructionByDeviceId( deviceUuid: string, projectUuid: string, From 712b7443acb7251ef1e5983ab6d7cd291f40e12d Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Thu, 10 Jul 2025 11:34:50 +0300 Subject: [PATCH 4/6] fix: prevent conditions overlapping by adding parenthesis to search condition (#464) --- src/booking/services/bookable-space.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/booking/services/bookable-space.service.ts b/src/booking/services/bookable-space.service.ts index ae8330b..0379214 100644 --- a/src/booking/services/bookable-space.service.ts +++ b/src/booking/services/bookable-space.service.ts @@ -52,7 +52,7 @@ export class BookableSpaceService { if (search) { qb = qb.andWhere( - 'space.spaceName ILIKE :search OR community.name ILIKE :search OR parentSpace.spaceName ILIKE :search', + '(space.spaceName ILIKE :search OR community.name ILIKE :search OR parentSpace.spaceName ILIKE :search)', { search: `%${search}%` }, ); } @@ -68,7 +68,6 @@ export class BookableSpaceService { .leftJoinAndSelect('space.bookableConfig', 'bookableConfig') .andWhere('bookableConfig.uuid IS NULL'); } - const customModel = TypeORMCustomModel(this.spaceRepository); const { baseResponseDto, paginationResponseDto } = From a17a271213cb9e22219fba6a7b9b611fce040081 Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Thu, 10 Jul 2025 14:41:50 +0300 Subject: [PATCH 5/6] add validation checks (#466) --- src/space/controllers/space.controller.ts | 10 ++++++++-- src/space/services/space.service.ts | 24 ++++++++++++++++++++++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/space/controllers/space.controller.ts b/src/space/controllers/space.controller.ts index 20660ca..776c222 100644 --- a/src/space/controllers/space.controller.ts +++ b/src/space/controllers/space.controller.ts @@ -1,11 +1,13 @@ import { ControllerRoute } from '@app/common/constants/controller-route'; import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; import { Body, Controller, Delete, Get, Param, + ParseUUIDPipe, Post, Put, Query, @@ -81,9 +83,13 @@ export class SpaceController { async updateSpacesOrder( @Body() orderSpacesDto: OrderSpacesDto, @Param() communitySpaceParam: CommunitySpaceParam, - @Param('parentSpaceUuid') parentSpaceUuid: string, + @Param('parentSpaceUuid', ParseUUIDPipe) parentSpaceUuid: string, ) { - return this.spaceService.updateSpacesOrder(parentSpaceUuid, orderSpacesDto); + await this.spaceService.updateSpacesOrder(parentSpaceUuid, orderSpacesDto); + return new SuccessResponseDto({ + statusCode: 200, + message: 'Spaces order updated successfully', + }); } @ApiBearerAuth() diff --git a/src/space/services/space.service.ts b/src/space/services/space.service.ts index 5ea39cd..126a3ec 100644 --- a/src/space/services/space.service.ts +++ b/src/space/services/space.service.ts @@ -360,6 +360,28 @@ export class SpaceService { parentSpaceUuid: string, { spacesUuids }: OrderSpacesDto, ) { + const parentSpace = await this.spaceRepository.findOne({ + where: { uuid: parentSpaceUuid, disabled: false }, + relations: ['children'], + }); + if (!parentSpace) { + throw new HttpException( + `Parent space with ID ${parentSpaceUuid} not found`, + HttpStatus.NOT_FOUND, + ); + } + // ensure that all sent spaces belong to the parent space + const missingSpaces = spacesUuids.filter( + (uuid) => !parentSpace.children.some((child) => child.uuid === uuid), + ); + if (missingSpaces.length > 0) { + throw new HttpException( + `Some spaces with IDs ${missingSpaces.join( + ', ', + )} do not belong to the parent space with ID ${parentSpaceUuid}`, + HttpStatus.BAD_REQUEST, + ); + } try { await this.spaceRepository.update( { uuid: In(spacesUuids), parent: { uuid: parentSpaceUuid } }, @@ -372,7 +394,7 @@ export class SpaceService { ' END', }, ); - return true; + return; } catch (error) { console.error('Error updating spaces order:', error); throw new HttpException( From a9cb1b670449b5ade935c9e1b71cee1b6cb0c90e Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Thu, 10 Jul 2025 15:41:46 +0300 Subject: [PATCH 6/6] SP-1830: add company name to invite user entity (#468) --- .../Invite-user/dtos/Invite-user.dto.ts | 10 +- .../entities/Invite-user.entity.ts | 5 + src/invite-user/dtos/add.invite-user.dto.ts | 16 +- .../dtos/update.invite-user.dto.ts | 80 +- .../services/invite-user.service.ts | 905 ++++++++++-------- src/project/services/project-user.service.ts | 2 + src/users/services/user-space.service.ts | 35 +- 7 files changed, 543 insertions(+), 510 deletions(-) diff --git a/libs/common/src/modules/Invite-user/dtos/Invite-user.dto.ts b/libs/common/src/modules/Invite-user/dtos/Invite-user.dto.ts index 50e826c..0760206 100644 --- a/libs/common/src/modules/Invite-user/dtos/Invite-user.dto.ts +++ b/libs/common/src/modules/Invite-user/dtos/Invite-user.dto.ts @@ -1,6 +1,6 @@ import { RoleType } from '@app/common/constants/role.type.enum'; import { UserStatusEnum } from '@app/common/constants/user-status.enum'; -import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; +import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; export class InviteUserDto { @IsString() @@ -12,8 +12,12 @@ export class InviteUserDto { public email: string; @IsString() - @IsNotEmpty() - public jobTitle: string; + @IsOptional() + public jobTitle?: string; + + @IsString() + @IsOptional() + public companyName?: string; @IsEnum(UserStatusEnum) @IsNotEmpty() 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 c798a70..07b3151 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 @@ -37,6 +37,11 @@ export class InviteUserEntity extends AbstractEntity { }) jobTitle: string; + @Column({ + nullable: true, + }) + companyName: string; + @Column({ nullable: false, enum: Object.values(UserStatusEnum), diff --git a/src/invite-user/dtos/add.invite-user.dto.ts b/src/invite-user/dtos/add.invite-user.dto.ts index 0d9acbc..689720c 100644 --- a/src/invite-user/dtos/add.invite-user.dto.ts +++ b/src/invite-user/dtos/add.invite-user.dto.ts @@ -5,6 +5,7 @@ import { IsNotEmpty, IsOptional, IsString, + IsUUID, } from 'class-validator'; export class AddUserInvitationDto { @@ -44,6 +45,15 @@ export class AddUserInvitationDto { @IsOptional() public jobTitle?: string; + @ApiProperty({ + description: 'The company name of the user', + example: 'Tech Corp', + required: false, + }) + @IsString() + @IsOptional() + public companyName?: string; + @ApiProperty({ description: 'The phone number of the user', example: '+1234567890', @@ -58,7 +68,7 @@ export class AddUserInvitationDto { example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', required: true, }) - @IsString() + @IsUUID('4') @IsNotEmpty() public roleUuid: string; @ApiProperty({ @@ -66,15 +76,17 @@ export class AddUserInvitationDto { example: 'd290f1ee-6c54-4b01-90e6-d701748f0851', required: true, }) - @IsString() + @IsUUID('4') @IsNotEmpty() public projectUuid: string; + @ApiProperty({ description: 'The array of space UUIDs (at least one required)', example: ['b5f3c9d2-58b7-4377-b3f7-60acb711d5d9'], required: true, }) @IsArray() + @IsUUID('4', { each: true }) @ArrayMinSize(1) public spaceUuids: string[]; constructor(dto: Partial) { diff --git a/src/invite-user/dtos/update.invite-user.dto.ts b/src/invite-user/dtos/update.invite-user.dto.ts index 6890ed1..fb981cd 100644 --- a/src/invite-user/dtos/update.invite-user.dto.ts +++ b/src/invite-user/dtos/update.invite-user.dto.ts @@ -1,78 +1,10 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { - ArrayMinSize, - IsArray, - IsBoolean, - IsNotEmpty, - IsOptional, - IsString, -} from 'class-validator'; +import { ApiProperty, OmitType } from '@nestjs/swagger'; +import { IsBoolean, IsNotEmpty, IsString } from 'class-validator'; +import { AddUserInvitationDto } from './add.invite-user.dto'; -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); - } -} +export class UpdateUserInvitationDto extends OmitType(AddUserInvitationDto, [ + 'email', +]) {} export class DisableUserInvitationDto { @ApiProperty({ description: 'The disable status of the user', diff --git a/src/invite-user/services/invite-user.service.ts b/src/invite-user/services/invite-user.service.ts index 4042663..10f1e5e 100644 --- a/src/invite-user/services/invite-user.service.ts +++ b/src/invite-user/services/invite-user.service.ts @@ -8,6 +8,8 @@ import { InviteUserRepository, InviteUserSpaceRepository, } from '@app/common/modules/Invite-user/repositiories'; +import { ProjectEntity } from '@app/common/modules/project/entities'; +import { RoleTypeEntity } from '@app/common/modules/role-type/entities'; import { RoleTypeRepository } from '@app/common/modules/role-type/repositories'; import { SpaceRepository } from '@app/common/modules/space'; import { SpaceEntity } from '@app/common/modules/space/entities/space.entity'; @@ -61,6 +63,7 @@ export class InviteUserService { lastName, email, jobTitle, + companyName, phoneNumber, roleUuid, spaceUuids, @@ -90,6 +93,8 @@ export class InviteUserService { ); } + await this.checkRole(roleUuid, queryRunner); + await this.checkProject(projectUuid, queryRunner); // Validate spaces const validSpaces = await this.validateSpaces( spaceUuids, @@ -102,6 +107,7 @@ export class InviteUserService { lastName, email, jobTitle, + companyName, phoneNumber, roleType: { uuid: roleUuid }, status: UserStatusEnum.INVITED, @@ -157,185 +163,6 @@ export class InviteUserService { await queryRunner.release(); } } - private async validateSpaces( - spaceUuids: string[], - entityManager: EntityManager, - ): Promise { - const spaceRepo = entityManager.getRepository(SpaceEntity); - - const validSpaces = await spaceRepo.find({ - where: { uuid: In(spaceUuids) }, - }); - - if (validSpaces.length !== spaceUuids.length) { - const validSpaceUuids = validSpaces.map((space) => space.uuid); - const invalidSpaceUuids = spaceUuids.filter( - (uuid) => !validSpaceUuids.includes(uuid), - ); - throw new HttpException( - `Invalid space UUIDs: ${invalidSpaceUuids.join(', ')}`, - HttpStatus.BAD_REQUEST, - ); - } - - return validSpaces; - } - - async checkEmailAndProject(dto: CheckEmailDto): Promise { - const { email } = dto; - - try { - const user = await this.userRepository.findOne({ - where: { email }, - relations: ['project'], - }); - this.validateUserOrInvite(user, 'User'); - const invitedUser = await this.inviteUserRepository.findOne({ - where: { email }, - relations: ['project'], - }); - this.validateUserOrInvite(invitedUser, 'Invited User'); - - return new SuccessResponseDto({ - statusCode: HttpStatus.OK, - success: true, - message: 'Valid email', - }); - } catch (error) { - console.error('Error checking email and project:', error); - throw new HttpException( - error.message || - 'An unexpected error occurred while checking the email', - error.status || HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - private validateUserOrInvite(user: any, userType: string): void { - if (user) { - if (!user.isActive) { - throw new HttpException( - `${userType} is deleted`, - HttpStatus.BAD_REQUEST, - ); - } - if (user.project) { - throw new HttpException( - `${userType} already has a project`, - HttpStatus.BAD_REQUEST, - ); - } - } - } - - async activationCode(dto: ActivateCodeDto): Promise { - const { activationCode, userUuid } = dto; - - try { - const user = await this.getUser(userUuid); - - const invitedUser = await this.inviteUserRepository.findOne({ - where: { - email: user.email, - status: UserStatusEnum.INVITED, - isActive: true, - }, - relations: ['project', 'spaces.space.community', 'roleType'], - }); - - if (invitedUser) { - if (invitedUser.invitationCode !== activationCode) { - throw new HttpException( - 'Invalid activation code', - HttpStatus.BAD_REQUEST, - ); - } - - // Handle invited user with valid activation code - await this.handleInvitedUser(user, invitedUser); - } else { - // Handle case for non-invited user - await this.handleNonInvitedUser(activationCode, userUuid); - } - 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, - ); - } - } - - private async getUser(userUuid: string): Promise { - const user = await this.userRepository.findOne({ - where: { uuid: userUuid, isActive: true, isUserVerified: true }, - }); - - if (!user) { - throw new HttpException('User not found', HttpStatus.NOT_FOUND); - } - return user; - } - - private async handleNonInvitedUser( - activationCode: string, - userUuid: string, - ): Promise { - await this.userSpaceService.verifyCodeAndAddUserSpace( - { inviteCode: activationCode }, - userUuid, - ); - } - - private async handleInvitedUser( - user: UserEntity, - invitedUser: InviteUserEntity, - ): Promise { - for (const invitedSpace of invitedUser.spaces) { - try { - const deviceUUIDs = await this.userSpaceService.getDeviceUUIDsForSpace( - invitedSpace.space.uuid, - ); - - await this.userSpaceService.addUserPermissionsToDevices( - user.uuid, - 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, - ); - continue; // Skip to the next space - } - } - - // Update invited user and associated user data - await this.inviteUserRepository.update( - { uuid: invitedUser.uuid }, - { status: UserStatusEnum.ACTIVE, user: { uuid: user.uuid } }, - ); - await this.userRepository.update( - { uuid: user.uuid }, - { - project: { uuid: invitedUser.project.uuid }, - roleType: { uuid: invitedUser.roleType.uuid }, - }, - ); - } async updateUserInvitation( dto: UpdateUserInvitationDto, @@ -357,6 +184,9 @@ export class InviteUserService { throw new HttpException('User not found', HttpStatus.NOT_FOUND); } + await this.checkRole(dto.roleUuid, queryRunner); + await this.checkProject(projectUuid, queryRunner); + // Perform update actions if status is 'INVITED' if (userOldData.status === UserStatusEnum.INVITED) { await this.updateWhenUserIsInvite(queryRunner, dto, invitedUserUuid); @@ -439,174 +269,7 @@ export class InviteUserService { await queryRunner.release(); } } - private async getRoleTypeByUuid(roleUuid: string) { - const role = await this.roleTypeRepository.findOne({ - where: { uuid: roleUuid }, - }); - return role.type; - } - - private async updateWhenUserIsInvite( - 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 userData = await this.inviteUserRepository.findOne({ - where: { uuid: invitedUserUuid }, - relations: ['user.userSpaces.space', 'user.userSpaces.space.community'], - }); - - if (!userData) { - 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 }, - }, - ); - await this.userRepository.update( - { uuid: userData.user.uuid }, - { - roleType: { uuid: roleUuid }, - }, - ); - // Disassociate the user from all current spaces - const disassociatePromises = userData.user.userSpaces.map((userSpace) => - this.spaceUserService - .disassociateUserFromSpace({ - communityUuid: userSpace.space.community.uuid, - spaceUuid: userSpace.space.uuid, - userUuid: userData.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( - userData.user.uuid, - deviceUUIDs, - ); - - // Associate the user with the new space - await this.spaceUserService.associateUserToSpace({ - communityUuid: spaceDetails.communityUuid, - spaceUuid: spaceUuid, - userUuid: userData.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); - } - } - } async disableUserInvitation( dto: DisableUserInvitationDto, invitedUserUuid: string, @@ -686,74 +349,6 @@ export class InviteUserService { } } - private async updateUserStatus( - invitedUserUuid: string, - projectUuid: string, - isEnabled: boolean, - ) { - await this.inviteUserRepository.update( - { uuid: invitedUserUuid, project: { uuid: projectUuid } }, - { isEnabled }, - ); - } - - 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); - } - async deleteUserInvitation( invitedUserUuid: string, ): Promise { @@ -824,4 +419,486 @@ export class InviteUserService { await queryRunner.release(); } } + + async activationCode(dto: ActivateCodeDto): Promise { + const { activationCode, userUuid } = dto; + + try { + const user = await this.getUser(userUuid); + + const invitedUser = await this.inviteUserRepository.findOne({ + where: { + email: user.email, + status: UserStatusEnum.INVITED, + isActive: true, + }, + relations: ['project', 'spaces.space.community', 'roleType'], + }); + + if (invitedUser) { + if (invitedUser.invitationCode !== activationCode) { + throw new HttpException( + 'Invalid activation code', + HttpStatus.BAD_REQUEST, + ); + } + + // Handle invited user with valid activation code + await this.handleInvitedUser(user, invitedUser); + } else { + // Handle case for non-invited user + await this.handleNonInvitedUser(activationCode, userUuid); + } + 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, + ); + } + } + + async checkEmailAndProject(dto: CheckEmailDto): Promise { + const { email } = dto; + + try { + const user = await this.userRepository.findOne({ + where: { email }, + relations: ['project'], + }); + this.validateUserOrInvite(user, 'User'); + const invitedUser = await this.inviteUserRepository.findOne({ + where: { email }, + relations: ['project'], + }); + this.validateUserOrInvite(invitedUser, 'Invited User'); + + return new SuccessResponseDto({ + statusCode: HttpStatus.OK, + success: true, + message: 'Valid email', + }); + } catch (error) { + console.error('Error checking email and project:', error); + throw new HttpException( + error.message || + 'An unexpected error occurred while checking the email', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + private 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); + } + } + } + + private async validateSpaces( + spaceUuids: string[], + entityManager: EntityManager, + ): Promise { + const spaceRepo = entityManager.getRepository(SpaceEntity); + + const validSpaces = await spaceRepo.find({ + where: { uuid: In(spaceUuids) }, + }); + + if (validSpaces.length !== spaceUuids.length) { + const validSpaceUuids = validSpaces.map((space) => space.uuid); + const invalidSpaceUuids = spaceUuids.filter( + (uuid) => !validSpaceUuids.includes(uuid), + ); + throw new HttpException( + `Invalid space UUIDs: ${invalidSpaceUuids.join(', ')}`, + HttpStatus.BAD_REQUEST, + ); + } + + return validSpaces; + } + + private async checkRole( + roleUuid: string, + queryRunner: QueryRunner, + ): Promise { + try { + const role = await queryRunner.manager.findOne(RoleTypeEntity, { + where: { uuid: roleUuid }, + }); + + if (!role) { + throw new HttpException('Role not found', HttpStatus.NOT_FOUND); + } + + return new SuccessResponseDto({ + statusCode: HttpStatus.OK, + success: true, + message: 'Valid role', + }); + } catch (error) { + console.error('Error checking role:', error); + throw new HttpException( + error.message || 'An unexpected error occurred while checking the role', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + private async checkProject( + projectUuid: string, + queryRunner: QueryRunner, + ): Promise { + try { + const project = await queryRunner.manager.findOne(ProjectEntity, { + where: { uuid: projectUuid }, + }); + + if (!project) { + throw new HttpException('Project not found', HttpStatus.NOT_FOUND); + } + + return new SuccessResponseDto({ + statusCode: HttpStatus.OK, + success: true, + message: 'Valid project', + }); + } catch (error) { + console.error('Error checking project:', error); + throw new HttpException( + error.message || + 'An unexpected error occurred while checking the project', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + private validateUserOrInvite(user: any, userType: string): void { + if (user) { + if (!user.isActive) { + throw new HttpException( + `${userType} is deleted`, + HttpStatus.BAD_REQUEST, + ); + } + if (user.project) { + throw new HttpException( + `${userType} already has a project`, + HttpStatus.BAD_REQUEST, + ); + } + } + } + + private async getUser(userUuid: string): Promise { + const user = await this.userRepository.findOne({ + where: { uuid: userUuid, isActive: true, isUserVerified: true }, + }); + + if (!user) { + throw new HttpException('User not found', HttpStatus.NOT_FOUND); + } + return user; + } + + private async handleNonInvitedUser( + activationCode: string, + userUuid: string, + ): Promise { + await this.userSpaceService.verifyCodeAndAddUserSpace( + { inviteCode: activationCode }, + userUuid, + ); + } + + private async handleInvitedUser( + user: UserEntity, + invitedUser: InviteUserEntity, + ): Promise { + for (const invitedSpace of invitedUser.spaces) { + try { + const deviceUUIDs = await this.userSpaceService.getDeviceUUIDsForSpace( + invitedSpace.space.uuid, + ); + + await this.userSpaceService.addUserPermissionsToDevices( + user.uuid, + 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, + ); + continue; // Skip to the next space + } + } + + // Update invited user and associated user data + await this.inviteUserRepository.update( + { uuid: invitedUser.uuid }, + { status: UserStatusEnum.ACTIVE, user: { uuid: user.uuid } }, + ); + await this.userRepository.update( + { uuid: user.uuid }, + { + project: { uuid: invitedUser.project.uuid }, + roleType: { uuid: invitedUser.roleType.uuid }, + }, + ); + } + + private async getRoleTypeByUuid(roleUuid: string) { + const role = await this.roleTypeRepository.findOne({ + where: { uuid: roleUuid }, + }); + + return role.type; + } + + private async updateWhenUserIsInvite( + queryRunner: QueryRunner, + dto: UpdateUserInvitationDto, + invitedUserUuid: string, + ): Promise { + const { + firstName, + lastName, + jobTitle, + companyName, + phoneNumber, + roleUuid, + spaceUuids, + } = dto; + + // Update user invitation details + await queryRunner.manager.update( + this.inviteUserRepository.target, + { uuid: invitedUserUuid }, + { + firstName, + lastName, + jobTitle, + companyName, + 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, + companyName, + phoneNumber, + roleUuid, + spaceUuids, + projectUuid, + } = dto; + + const userData = await this.inviteUserRepository.findOne({ + where: { uuid: invitedUserUuid }, + relations: ['user.userSpaces.space', 'user.userSpaces.space.community'], + }); + + if (!userData) { + 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, + companyName, + phoneNumber, + roleType: { uuid: roleUuid }, + }, + ); + await this.userRepository.update( + { uuid: userData.user.uuid }, + { + roleType: { uuid: roleUuid }, + }, + ); + // Disassociate the user from all current spaces + const disassociatePromises = userData.user.userSpaces.map((userSpace) => + this.spaceUserService + .disassociateUserFromSpace({ + communityUuid: userSpace.space.community.uuid, + spaceUuid: userSpace.space.uuid, + userUuid: userData.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( + userData.user.uuid, + deviceUUIDs, + ); + + // Associate the user with the new space + await this.spaceUserService.associateUserToSpace({ + communityUuid: spaceDetails.communityUuid, + spaceUuid: spaceUuid, + userUuid: userData.user.uuid, + projectUuid, + }); + } catch (error) { + console.error(`Failed to process space ${spaceUuid}:`, error); + throw error; + } + }); + + await Promise.all(associatePromises); + } + + private async updateUserStatus( + invitedUserUuid: string, + projectUuid: string, + isEnabled: boolean, + ) { + await this.inviteUserRepository.update( + { uuid: invitedUserUuid, project: { uuid: projectUuid } }, + { isEnabled }, + ); + } + + 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); + } } diff --git a/src/project/services/project-user.service.ts b/src/project/services/project-user.service.ts index f6a1dcb..3b1d4a2 100644 --- a/src/project/services/project-user.service.ts +++ b/src/project/services/project-user.service.ts @@ -33,6 +33,7 @@ export class ProjectUserService { 'status', 'phoneNumber', 'jobTitle', + 'companyName', 'invitedBy', 'isEnabled', ], @@ -91,6 +92,7 @@ export class ProjectUserService { 'status', 'phoneNumber', 'jobTitle', + 'companyName', 'invitedBy', 'isEnabled', ], diff --git a/src/users/services/user-space.service.ts b/src/users/services/user-space.service.ts index e0a5b79..7b9cbb5 100644 --- a/src/users/services/user-space.service.ts +++ b/src/users/services/user-space.service.ts @@ -1,28 +1,28 @@ +import { CommonErrorCodes } from '@app/common/constants/error-codes.enum'; +import { PermissionType } from '@app/common/constants/permission-type.enum'; +import { RoleType } from '@app/common/constants/role.type.enum'; +import { UserStatusEnum } from '@app/common/constants/user-status.enum'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; +import { + InviteUserRepository, + InviteUserSpaceRepository, +} from '@app/common/modules/Invite-user/repositiories'; +import { InviteSpaceEntity } from '@app/common/modules/space/entities/invite-space.entity'; +import { + InviteSpaceRepository, + SpaceRepository, +} from '@app/common/modules/space/repositories'; +import { UserSpaceRepository } from '@app/common/modules/user/repositories'; import { BadRequestException, HttpException, HttpStatus, Injectable, } from '@nestjs/common'; -import { UserSpaceRepository } from '@app/common/modules/user/repositories'; -import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; -import { BaseResponseDto } from '@app/common/dto/base.response.dto'; -import { AddUserSpaceDto, AddUserSpaceUsingCodeDto } from '../dtos'; -import { - InviteSpaceRepository, - SpaceRepository, -} from '@app/common/modules/space/repositories'; -import { CommonErrorCodes } from '@app/common/constants/error-codes.enum'; import { UserDevicePermissionService } from 'src/user-device-permission/services'; -import { PermissionType } from '@app/common/constants/permission-type.enum'; -import { InviteSpaceEntity } from '@app/common/modules/space/entities/invite-space.entity'; +import { AddUserSpaceDto, AddUserSpaceUsingCodeDto } from '../dtos'; import { UserService } from './user.service'; -import { RoleType } from '@app/common/constants/role.type.enum'; -import { - InviteUserRepository, - InviteUserSpaceRepository, -} from '@app/common/modules/Invite-user/repositiories'; -import { UserStatusEnum } from '@app/common/constants/user-status.enum'; @Injectable() export class UserSpaceService { @@ -154,6 +154,7 @@ export class UserSpaceService { lastName: user.lastName, email: user.email, jobTitle: null, + companyName: null, phoneNumber: null, roleType: { uuid: user.role.uuid }, status: UserStatusEnum.ACTIVE,