diff --git a/.env.example b/.env.example index 9525e51..c194e08 100644 --- a/.env.example +++ b/.env.example @@ -58,6 +58,14 @@ MAILTRAP_API_TOKEN= MAILTRAP_INVITATION_TEMPLATE_UUID= +MAILTRAP_EDIT_USER_TEMPLATE_UUID= + +MAILTRAP_DISABLE_TEMPLATE_UUID= + +MAILTRAP_ENABLE_TEMPLATE_UUID= + +MAILTRAP_DELETE_USER_TEMPLATE_UUID= + WEBSITES_ENABLE_APP_SERVICE_STORAGE= PORT= diff --git a/libs/common/src/auth/services/auth.service.ts b/libs/common/src/auth/services/auth.service.ts index 7f4f03a..95d592e 100644 --- a/libs/common/src/auth/services/auth.service.ts +++ b/libs/common/src/auth/services/auth.service.ts @@ -45,6 +45,9 @@ export class AuthService { if (!user.isUserVerified) { throw new BadRequestException('User is not verified'); } + if (!user.isActive) { + throw new BadRequestException('User is not active'); + } if (user) { const passwordMatch = this.helperHashService.bcryptCompare( pass, diff --git a/libs/common/src/config/email.config.ts b/libs/common/src/config/email.config.ts index e18d4e8..7c9b776 100644 --- a/libs/common/src/config/email.config.ts +++ b/libs/common/src/config/email.config.ts @@ -13,5 +13,11 @@ export default registerAs( MAILTRAP_API_TOKEN: process.env.MAILTRAP_API_TOKEN, MAILTRAP_INVITATION_TEMPLATE_UUID: process.env.MAILTRAP_INVITATION_TEMPLATE_UUID, + MAILTRAP_DISABLE_TEMPLATE_UUID: process.env.MAILTRAP_DISABLE_TEMPLATE_UUID, + MAILTRAP_ENABLE_TEMPLATE_UUID: process.env.MAILTRAP_ENABLE_TEMPLATE_UUID, + MAILTRAP_DELETE_USER_TEMPLATE_UUID: + process.env.MAILTRAP_DELETE_USER_TEMPLATE_UUID, + MAILTRAP_EDIT_USER_TEMPLATE_UUID: + process.env.MAILTRAP_EDIT_USER_TEMPLATE_UUID, }), ); diff --git a/libs/common/src/constants/controller-route.ts b/libs/common/src/constants/controller-route.ts index 934414d..67a0085 100644 --- a/libs/common/src/constants/controller-route.ts +++ b/libs/common/src/constants/controller-route.ts @@ -766,6 +766,18 @@ 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 DELETE_USER_INVITATION_SUMMARY = + 'Delete user invitation'; + + public static readonly DELETE_USER_INVITATION_DESCRIPTION = + 'This endpoint deletes an invitation for a user to assign to role and spaces.'; + public static readonly ACTIVATION_CODE_SUMMARY = 'Activate Invitation Code'; diff --git a/libs/common/src/constants/user-status.enum.ts b/libs/common/src/constants/user-status.enum.ts index b0b9817..859fb04 100644 --- a/libs/common/src/constants/user-status.enum.ts +++ b/libs/common/src/constants/user-status.enum.ts @@ -1,5 +1,4 @@ export enum UserStatusEnum { ACTIVE = 'active', INVITED = 'invited', - DISABLED = 'disabled', } 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 491d16f..22bbbdb 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 @@ -61,6 +61,11 @@ export class InviteUserEntity extends AbstractEntity { default: true, }) public isActive: boolean; + @Column({ + nullable: false, + default: true, + }) + public isEnabled: boolean; @Column({ nullable: false, unique: true, diff --git a/libs/common/src/util/email.service.ts b/libs/common/src/util/email.service.ts index fd84ea6..c6e09ec 100644 --- a/libs/common/src/util/email.service.ts +++ b/libs/common/src/util/email.service.ts @@ -83,4 +83,137 @@ export class EmailService { ); } } + async sendEmailWithTemplate( + email: string, + name: string, + isEnable: boolean, + isDelete: boolean, + ): Promise { + const isProduction = process.env.NODE_ENV === 'production'; + const API_TOKEN = this.configService.get( + 'email-config.MAILTRAP_API_TOKEN', + ); + const API_URL = isProduction + ? SEND_EMAIL_API_URL_PROD + : SEND_EMAIL_API_URL_DEV; + + // Determine the template UUID based on the arguments + const templateUuid = isDelete + ? this.configService.get( + 'email-config.MAILTRAP_DELETE_USER_TEMPLATE_UUID', + ) + : this.configService.get( + isEnable + ? 'email-config.MAILTRAP_ENABLE_TEMPLATE_UUID' + : 'email-config.MAILTRAP_DISABLE_TEMPLATE_UUID', + ); + + const emailData = { + from: { + email: this.smtpConfig.sender, + }, + to: [ + { + email, + }, + ], + template_uuid: templateUuid, + template_variables: { + name, + }, + }; + + try { + await axios.post(API_URL, emailData, { + headers: { + Authorization: `Bearer ${API_TOKEN}`, + 'Content-Type': 'application/json', + }, + }); + } catch (error) { + throw new HttpException( + error.response?.data?.message || + 'Error sending email using Mailtrap template', + error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + async sendEditUserEmailWithTemplate( + email: string, + emailEditData: any, + ): Promise { + const isProduction = process.env.NODE_ENV === 'production'; + const API_TOKEN = this.configService.get( + 'email-config.MAILTRAP_API_TOKEN', + ); + const API_URL = isProduction + ? SEND_EMAIL_API_URL_PROD + : SEND_EMAIL_API_URL_DEV; + const TEMPLATE_UUID = this.configService.get( + 'email-config.MAILTRAP_EDIT_USER_TEMPLATE_UUID', + ); + + const emailData = { + from: { + email: this.smtpConfig.sender, + }, + to: [ + { + email: email, + }, + ], + template_uuid: TEMPLATE_UUID, + template_variables: emailEditData, + }; + + try { + await axios.post(API_URL, emailData, { + headers: { + Authorization: `Bearer ${API_TOKEN}`, + 'Content-Type': 'application/json', + }, + }); + } catch (error) { + throw new HttpException( + error.response?.data?.message || + 'Error sending email using Mailtrap template', + error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + generateUserChangesEmailBody( + addedSpaceNames: string[], + removedSpaceNames: string[], + oldRole: string, + newRole: string, + oldName: string, + newName: string, + ) { + const addedSpaceNamesChanged = + addedSpaceNames.length > 0 + ? `Access to the following spaces were added: ${addedSpaceNames.join(', ')}` + : ''; + + const removedSpaceNamesChanged = + removedSpaceNames.length > 0 + ? `Access to the following spaces were deleted: ${removedSpaceNames.join(', ')}` + : ''; + + const roleChanged = + oldRole !== newRole + ? `Your user role has been changed from [${oldRole}] to [${newRole}]` + : ''; + + const nameChanged = + oldName !== newName + ? `The name associated with your account has changed from [${oldName}] to [${newName}]` + : ''; + + return { + addedSpaceNamesChanged, + removedSpaceNamesChanged, + roleChanged, + nameChanged, + }; + } } diff --git a/src/auth/services/user-auth.service.ts b/src/auth/services/user-auth.service.ts index 89eda76..afdf6d2 100644 --- a/src/auth/services/user-auth.service.ts +++ b/src/auth/services/user-auth.service.ts @@ -151,7 +151,7 @@ export class UserAuthService { }); return res; } catch (error) { - throw new BadRequestException('Invalid credentials'); + throw new BadRequestException(error.message || 'Invalid credentials'); } } diff --git a/src/invite-user/controllers/invite-user.controller.ts b/src/invite-user/controllers/invite-user.controller.ts index 9da2224..8f65993 100644 --- a/src/invite-user/controllers/invite-user.controller.ts +++ b/src/invite-user/controllers/invite-user.controller.ts @@ -2,6 +2,7 @@ import { InviteUserService } from '../services/invite-user.service'; import { Body, Controller, + Delete, Param, Post, Put, @@ -17,7 +18,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 +97,35 @@ 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, + ); + } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Delete(':invitedUserUuid') + @ApiOperation({ + summary: ControllerRoute.INVITE_USER.ACTIONS.DELETE_USER_INVITATION_SUMMARY, + description: + ControllerRoute.INVITE_USER.ACTIONS.DELETE_USER_INVITATION_DESCRIPTION, + }) + async deleteUserInvitation( + @Param('invitedUserUuid') invitedUserUuid: string, + ): Promise { + return await this.inviteUserService.deleteUserInvitation(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/invite-user.module.ts b/src/invite-user/invite-user.module.ts index 443b928..fc0b579 100644 --- a/src/invite-user/invite-user.module.ts +++ b/src/invite-user/invite-user.module.ts @@ -25,6 +25,7 @@ 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'; +import { RoleTypeRepository } from '@app/common/modules/role-type/repositories'; @Module({ imports: [ConfigModule, InviteUserRepositoryModule], @@ -49,6 +50,7 @@ import { ProjectUserService } from 'src/project/services/project-user.service'; DeviceUserPermissionRepository, PermissionTypeRepository, ProjectUserService, + RoleTypeRepository, ], exports: [InviteUserService], }) diff --git a/src/invite-user/services/invite-user.service.ts b/src/invite-user/services/invite-user.service.ts index fd41f96..c62a364 100644 --- a/src/invite-user/services/invite-user.service.ts +++ b/src/invite-user/services/invite-user.service.ts @@ -24,7 +24,11 @@ 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'; +import { RoleTypeRepository } from '@app/common/modules/role-type/repositories'; @Injectable() export class InviteUserService { @@ -36,6 +40,7 @@ export class InviteUserService { private readonly userSpaceService: UserSpaceService, private readonly spaceUserService: SpaceUserService, private readonly spaceRepository: SpaceRepository, + private readonly roleTypeRepository: RoleTypeRepository, private readonly dataSource: DataSource, ) {} @@ -111,7 +116,7 @@ export class InviteUserService { await Promise.all(spacePromises); await this.emailService.sendEmailWithInvitationTemplate(email, { - name: firstName + ' ' + lastName, + name: firstName, invitationCode, role: roleType, spacesList: spaceNamesString, @@ -269,7 +274,7 @@ export class InviteUserService { dto: UpdateUserInvitationDto, invitedUserUuid: string, ): Promise { - const { projectUuid } = dto; + const { projectUuid, spaceUuids } = dto; const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.startTransaction(); @@ -278,6 +283,7 @@ export class InviteUserService { // Fetch the user's existing data in the project const userOldData = await this.inviteUserRepository.findOne({ where: { uuid: invitedUserUuid, project: { uuid: projectUuid } }, + relations: ['project', 'spaces.space', 'roleType'], }); if (!userOldData) { @@ -285,19 +291,66 @@ export class InviteUserService { } // Perform update actions if status is 'INVITED' - if ( - userOldData.status === UserStatusEnum.INVITED || - userOldData.status === UserStatusEnum.DISABLED - ) { - await this.updateWhenUserIsInviteOrDisable( - queryRunner, - dto, - invitedUserUuid, - ); + if (userOldData.status === UserStatusEnum.INVITED) { + await this.updateWhenUserIsInvite(queryRunner, dto, invitedUserUuid); } else if (userOldData.status === UserStatusEnum.ACTIVE) { + await this.updateWhenUserIsInvite(queryRunner, dto, invitedUserUuid); await this.updateWhenUserIsActive(queryRunner, dto, invitedUserUuid); } + // Extract existing space UUIDs + const oldSpaceUuids = userOldData.spaces.map((space) => space.space.uuid); + // Compare spaces + const addedSpaces = spaceUuids.filter( + (uuid) => !oldSpaceUuids.includes(uuid), + ); + const removedSpaces = oldSpaceUuids.filter( + (uuid) => !spaceUuids.includes(uuid), + ); + + // Fetch the space names for added and removed spaces + const spaceRepo = queryRunner.manager.getRepository(SpaceEntity); + const addedSpacesDetails = await spaceRepo.find({ + where: { + uuid: In(addedSpaces), + }, + }); + + const removedSpacesDetails = await spaceRepo.find({ + where: { + uuid: In(removedSpaces), + }, + }); + + // Extract the names of the added and removed spaces + const addedSpaceNames = addedSpacesDetails.map( + (space) => space.spaceName, + ); + const removedSpaceNames = removedSpacesDetails.map( + (space) => space.spaceName, + ); + + // Check for role and name change + const oldRole = userOldData.roleType.type; + const newRole = await this.getRoleTypeByUuid(dto.roleUuid); + const oldFullName = `${userOldData.firstName} ${userOldData.lastName}`; + const newFullName = `${dto.firstName} ${dto.lastName}`; + + // Generate email body + const emailMessage = this.emailService.generateUserChangesEmailBody( + addedSpaceNames, + removedSpaceNames, + oldRole, + newRole, + oldFullName, + newFullName, + ); + await this.emailService.sendEditUserEmailWithTemplate( + userOldData.email, + emailMessage, + ); + + // Proceed with other updates (e.g., roles, names, etc.) await queryRunner.commitTransaction(); return new SuccessResponseDto({ @@ -319,8 +372,15 @@ export class InviteUserService { await queryRunner.release(); } } + private async getRoleTypeByUuid(roleUuid: string) { + const role = await this.roleTypeRepository.findOne({ + where: { uuid: roleUuid }, + }); - private async updateWhenUserIsInviteOrDisable( + return role.type; + } + + private async updateWhenUserIsInvite( queryRunner: QueryRunner, dto: UpdateUserInvitationDto, invitedUserUuid: string, @@ -475,4 +535,209 @@ 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 this.emailService.sendEmailWithTemplate( + userData.email, + userData.firstName, + disable, + false, + ); + 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); + } + + async deleteUserInvitation( + invitedUserUuid: string, + ): Promise { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.startTransaction(); + + try { + const userData = await this.inviteUserRepository.findOne({ + where: { uuid: invitedUserUuid }, + relations: ['roleType', 'spaces.space', 'project'], + }); + + if (!userData) { + throw new HttpException('User not found', HttpStatus.NOT_FOUND); + } + + if (userData.status === UserStatusEnum.INVITED) { + await this.inviteUserRepository.update( + { uuid: invitedUserUuid }, + { isActive: false }, + ); + } 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, + ); + } + + await this.disassociateUserFromSpaces(user, userData.project.uuid); + await this.inviteUserRepository.update( + { uuid: invitedUserUuid }, + { isActive: false }, + ); + await this.userRepository.update( + { uuid: user.uuid }, + { isActive: false }, + ); + } + await this.emailService.sendEmailWithTemplate( + userData.email, + userData.firstName, + false, + true, + ); + await queryRunner.commitTransaction(); + + return new SuccessResponseDto({ + statusCode: HttpStatus.OK, + success: true, + message: 'User invitation deleted successfully', + }); + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } + } } diff --git a/src/project/services/project-user.service.ts b/src/project/services/project-user.service.ts index d69332f..5564f13 100644 --- a/src/project/services/project-user.service.ts +++ b/src/project/services/project-user.service.ts @@ -3,7 +3,6 @@ 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() @@ -35,6 +34,7 @@ export class ProjectUserService { 'phoneNumber', 'jobTitle', 'invitedBy', + 'isEnabled', ], relations: ['roleType'], }); @@ -73,7 +73,6 @@ export class ProjectUserService { invitedUserUuid: string, ): Promise { try { - let userSpaces; const user = await this.inviteUserRepository.findOne({ where: { project: { uuid: projectUuid }, @@ -90,6 +89,7 @@ export class ProjectUserService { 'phoneNumber', 'jobTitle', 'invitedBy', + 'isEnabled', ], relations: ['roleType', 'spaces.space'], }); @@ -100,15 +100,7 @@ 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(); @@ -119,7 +111,7 @@ export class ProjectUserService { roleType: user.roleType.type, createdDate, createdTime, - spaces: userSpaces, + spaces: user.spaces.map((space) => space.space), }, statusCode: HttpStatus.OK, });